From: Marcel van der Veldt Date: Sat, 2 Nov 2024 16:20:16 +0000 (+0100) Subject: Feat: Reorganize repository to contain only the server code X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=36a706fcdf7a5a16e04fef5fe0d599b1ef1d37d3;p=music-assistant-server.git Feat: Reorganize repository to contain only the server code --- diff --git a/.release-please-config-dev.json b/.release-please-config-dev.json deleted file mode 100644 index 8d96b494..00000000 --- a/.release-please-config-dev.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "prerelease": true, - "versioning-strategy": "prerelease", - "prerelease-type": "b", - "draft": true - } - } -} diff --git a/.release-please-config-stable.json b/.release-please-config-stable.json deleted file mode 100644 index d1de93f0..00000000 --- a/.release-please-config-stable.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "draft": true - } - } -} diff --git a/.release-please-manifest-dev.json b/.release-please-manifest-dev.json deleted file mode 100644 index 11094440..00000000 --- a/.release-please-manifest-dev.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "2.4.0b2" -} diff --git a/.release-please-manifest-stable.json b/.release-please-manifest-stable.json deleted file mode 100644 index aca3a494..00000000 --- a/.release-please-manifest-stable.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "2.3.1" -} diff --git a/music_assistant/__init__.py b/music_assistant/__init__.py index 92538754..6632acb6 100644 --- a/music_assistant/__init__.py +++ b/music_assistant/__init__.py @@ -1 +1,3 @@ """Music Assistant: The music library manager in python.""" + +from .mass import MusicAssistant # noqa: F401 diff --git a/music_assistant/__main__.py b/music_assistant/__main__.py index 6f9ad40e..1e989c65 100644 --- a/music_assistant/__main__.py +++ b/music_assistant/__main__.py @@ -17,10 +17,10 @@ from typing import Any, Final from aiorun import run from colorlog import ColoredFormatter -from music_assistant.common.helpers.json import json_loads +from music_assistant import MusicAssistant from music_assistant.constants import MASS_LOGGER_NAME, VERBOSE_LOG_LEVEL -from music_assistant.server import MusicAssistant -from music_assistant.server.helpers.logging import activate_log_queue_handler +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.logging import activate_log_queue_handler FORMAT_DATE: Final = "%Y-%m-%d" FORMAT_TIME: Final = "%H:%M:%S" diff --git a/music_assistant/client/__init__.py b/music_assistant/client/__init__.py deleted file mode 100644 index 731b8d52..00000000 --- a/music_assistant/client/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Music Assistant Client: Manage a Music Assistant server remotely.""" - -from .client import MusicAssistantClient # noqa: F401 diff --git a/music_assistant/client/client.py b/music_assistant/client/client.py deleted file mode 100644 index 48d6cdba..00000000 --- a/music_assistant/client/client.py +++ /dev/null @@ -1,396 +0,0 @@ -"""Music Assistant Client: Manage a Music Assistant server remotely.""" - -from __future__ import annotations - -import asyncio -import logging -import urllib.parse -import uuid -from collections.abc import Callable, Coroutine -from typing import TYPE_CHECKING, Any - -from music_assistant.client.exceptions import ConnectionClosed, InvalidServerVersion, InvalidState -from music_assistant.common.models.api import ( - CommandMessage, - ErrorResultMessage, - EventMessage, - ResultMessageBase, - ServerInfoMessage, - SuccessResultMessage, - parse_message, -) -from music_assistant.common.models.enums import EventType, ImageType -from music_assistant.common.models.errors import ERROR_MAP -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.media_items import ItemMapping, MediaItemType -from music_assistant.common.models.provider import ProviderInstance, ProviderManifest -from music_assistant.common.models.queue_item import QueueItem -from music_assistant.constants import API_SCHEMA_VERSION - -from .config import Config -from .connection import WebsocketsConnection -from .music import Music -from .player_queues import PlayerQueues -from .players import Players - -if TYPE_CHECKING: - from types import TracebackType - - from aiohttp import ClientSession - - from music_assistant.common.models.media_items import MediaItemImage - -EventCallBackType = Callable[[MassEvent], Coroutine[Any, Any, None] | None] -EventSubscriptionType = tuple[ - EventCallBackType, tuple[EventType, ...] | None, tuple[str, ...] | None -] - - -class MusicAssistantClient: - """Manage a Music Assistant server remotely.""" - - def __init__(self, server_url: str, aiohttp_session: ClientSession | None) -> None: - """Initialize the Music Assistant client.""" - self.server_url = server_url - self.connection = WebsocketsConnection(server_url, aiohttp_session) - self.logger = logging.getLogger(__package__) - self._result_futures: dict[str | int, asyncio.Future[Any]] = {} - self._subscribers: list[EventSubscriptionType] = [] - self._stop_called: bool = False - self._loop: asyncio.AbstractEventLoop | None = None - self._config = Config(self) - self._players = Players(self) - self._player_queues = PlayerQueues(self) - self._music = Music(self) - # below items are retrieved after connect - self._server_info: ServerInfoMessage | None = None - self._provider_manifests: dict[str, ProviderManifest] = {} - self._providers: dict[str, ProviderInstance] = {} - - @property - def server_info(self) -> ServerInfoMessage | None: - """Return info of the server we're currently connected to.""" - return self._server_info - - @property - def providers(self) -> list[ProviderInstance]: - """Return all loaded/running Providers (instances).""" - return list(self._providers.values()) - - @property - def provider_manifests(self) -> list[ProviderManifest]: - """Return all Provider manifests.""" - return list(self._provider_manifests.values()) - - @property - def config(self) -> Config: - """Return Config handler.""" - return self._config - - @property - def players(self) -> Players: - """Return Players handler.""" - return self._players - - @property - def player_queues(self) -> PlayerQueues: - """Return PlayerQueues handler.""" - return self._player_queues - - @property - def music(self) -> Music: - """Return Music handler.""" - return self._music - - def get_provider_manifest(self, domain: str) -> ProviderManifest: - """Return Provider manifests of single provider(domain).""" - return self._provider_manifests[domain] - - def get_provider( - self, provider_instance_or_domain: str, return_unavailable: bool = False - ) -> ProviderInstance | None: - """Return provider by instance id or domain.""" - # lookup by instance_id first - if prov := self._providers.get(provider_instance_or_domain): - if return_unavailable or prov.available: - return prov - if not prov.is_streaming_provider: - # no need to lookup other instances because this provider has unique data - return None - provider_instance_or_domain = prov.domain - # fallback to match on domain - # note that this can be tricky if the provider has multiple instances - # and has unique data (e.g. filesystem) - for prov in self._providers.values(): - if prov.domain != provider_instance_or_domain: - continue - if return_unavailable or prov.available: - return prov - self.logger.debug("Provider %s is not available", provider_instance_or_domain) - return None - - def get_image_url(self, image: MediaItemImage, size: int = 0) -> str: - """Get (proxied) URL for MediaItemImage.""" - assert self.server_info - if image.remotely_accessible and not size: - return image.path - if image.remotely_accessible and size: - # get url to resized image(thumb) from weserv service - return ( - f"https://images.weserv.nl/?url={urllib.parse.quote(image.path)}" - f"&w=${size}&h=${size}&fit=cover&a=attention" - ) - # return imageproxy url for images that need to be resolved - # the original path is double encoded - encoded_url = urllib.parse.quote(urllib.parse.quote(image.path)) - return ( - f"{self.server_info.base_url}/imageproxy?path={encoded_url}" - f"&provider={image.provider}&size={size}" - ) - - def get_media_item_image_url( - self, - item: MediaItemType | ItemMapping | QueueItem, - type: ImageType = ImageType.THUMB, # noqa: A002 - size: int = 0, - ) -> str | None: - """Get image URL for MediaItem, QueueItem or ItemMapping.""" - # handle queueitem with media_item attribute - if media_item := getattr(item, "media_item", None): - if img := self.music.get_media_item_image(media_item, type): - return self.get_image_url(img, size) - if img := self.music.get_media_item_image(item, type): - return self.get_image_url(img, size) - return None - - def subscribe( - self, - cb_func: EventCallBackType, - event_filter: EventType | tuple[EventType, ...] | None = None, - id_filter: str | tuple[str, ...] | None = None, - ) -> Callable[[], None]: - """Add callback to event listeners. - - Returns function to remove the listener. - :param cb_func: callback function or coroutine - :param event_filter: Optionally only listen for these events - :param id_filter: Optionally only listen for these id's (player_id, queue_id, uri) - """ - if isinstance(event_filter, EventType): - event_filter = (event_filter,) - if isinstance(id_filter, str): - id_filter = (id_filter,) - listener = (cb_func, event_filter, id_filter) - self._subscribers.append(listener) - - def remove_listener() -> None: - self._subscribers.remove(listener) - - return remove_listener - - async def connect(self) -> None: - """Connect to the remote Music Assistant Server.""" - self._loop = asyncio.get_running_loop() - if self.connection.connected: - # already connected - return - # NOTE: connect will raise when connecting failed - result = await self.connection.connect() - info = ServerInfoMessage.from_dict(result) - - # basic check for server schema version compatibility - if info.min_supported_schema_version > API_SCHEMA_VERSION: - # our schema version is too low and can't be handled by the server anymore. - await self.connection.disconnect() - msg = ( - f"Schema version is incompatible: {info.schema_version}, " - f"the server requires at least {info.min_supported_schema_version} " - " - update the Music Assistant client to a more " - "recent version or downgrade the server." - ) - raise InvalidServerVersion(msg) - - self._server_info = info - - self.logger.info( - "Connected to Music Assistant Server %s, Version %s, Schema Version %s", - info.server_id, - info.server_version, - info.schema_version, - ) - - async def send_command( - self, - command: str, - require_schema: int | None = None, - **kwargs: Any, - ) -> Any: - """Send a command and get a response.""" - if not self.connection.connected or not self._loop: - msg = "Not connected" - raise InvalidState(msg) - - if ( - require_schema is not None - and self.server_info is not None - and require_schema > self.server_info.schema_version - ): - msg = ( - "Command not available due to incompatible server version. Update the Music " - f"Assistant Server to a version that supports at least api schema {require_schema}." - ) - raise InvalidServerVersion(msg) - - command_message = CommandMessage( - message_id=uuid.uuid4().hex, - command=command, - args=kwargs, - ) - future: asyncio.Future[Any] = self._loop.create_future() - self._result_futures[command_message.message_id] = future - await self.connection.send_message(command_message.to_dict()) - try: - return await future - finally: - self._result_futures.pop(command_message.message_id) - - async def send_command_no_wait( - self, - command: str, - require_schema: int | None = None, - **kwargs: Any, - ) -> None: - """Send a command without waiting for the response.""" - if not self.server_info: - msg = "Not connected" - raise InvalidState(msg) - - if require_schema is not None and require_schema > self.server_info.schema_version: - msg = ( - "Command not available due to incompatible server version. Update the Music " - f"Assistant Server to a version that supports at least api schema {require_schema}." - ) - raise InvalidServerVersion(msg) - command_message = CommandMessage( - message_id=uuid.uuid4().hex, - command=command, - args=kwargs, - ) - await self.connection.send_message(command_message.to_dict()) - - async def start_listening(self, init_ready: asyncio.Event | None = None) -> None: - """Connect (if needed) and start listening to incoming messages from the server.""" - await self.connect() - - # fetch initial state - # we do this in a separate task to not block reading messages - async def fetch_initial_state() -> None: - self._providers = { - x["instance_id"]: ProviderInstance.from_dict(x) - for x in await self.send_command("providers") - } - self._provider_manifests = { - x["domain"]: ProviderManifest.from_dict(x) - for x in await self.send_command("providers/manifests") - } - await self._player_queues.fetch_state() - await self._players.fetch_state() - - if init_ready is not None: - init_ready.set() - - asyncio.create_task(fetch_initial_state()) - - try: - # keep reading incoming messages - while not self._stop_called: - msg = await self.connection.receive_message() - self._handle_incoming_message(msg) - except ConnectionClosed: - pass - finally: - await self.disconnect() - - async def disconnect(self) -> None: - """Disconnect the client and cleanup.""" - self._stop_called = True - # cancel all command-tasks awaiting a result - for future in self._result_futures.values(): - future.cancel() - await self.connection.disconnect() - - def _handle_incoming_message(self, raw: dict[str, Any]) -> None: - """ - Handle incoming message. - - Run all async tasks in a wrapper to log appropriately. - """ - msg = parse_message(raw) - # handle result message - if isinstance(msg, ResultMessageBase): - future = self._result_futures.get(msg.message_id) - - if future is None: - # no listener for this result - return - if isinstance(msg, SuccessResultMessage): - future.set_result(msg.result) - return - if isinstance(msg, ErrorResultMessage): - exc = ERROR_MAP[msg.error_code] - future.set_exception(exc(msg.details)) - return - - # handle EventMessage - if isinstance(msg, EventMessage): - self.logger.debug("Received event: %s", msg) - self._handle_event(msg) - return - - # Log anything we can't handle here - self.logger.debug( - "Received message with unknown type '%s': %s", - type(msg), - msg, - ) - - def _handle_event(self, event: MassEvent) -> None: - """Forward event to subscribers.""" - if self._stop_called: - return - - assert self._loop - - if event.event == EventType.PROVIDERS_UPDATED: - self._providers = {x["instance_id"]: ProviderInstance.from_dict(x) for x in event.data} - - for cb_func, event_filter, id_filter in self._subscribers: - if not (event_filter is None or event.event in event_filter): - continue - if not (id_filter is None or event.object_id in id_filter): - continue - if asyncio.iscoroutinefunction(cb_func): - asyncio.run_coroutine_threadsafe(cb_func(event), self._loop) - else: - self._loop.call_soon_threadsafe(cb_func, event) - - async def __aenter__(self) -> MusicAssistantClient: - """Initialize and connect the connection to the Music Assistant Server.""" - await self.connect() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - await self.disconnect() - return None - - def __repr__(self) -> str: - """Return the representation.""" - conn_type = self.connection.__class__.__name__ - prefix = "" if self.connection.connected else "not " - return f"{type(self).__name__}(connection={conn_type}, {prefix}connected)" diff --git a/music_assistant/client/config.py b/music_assistant/client/config.py deleted file mode 100644 index 5a901934..00000000 --- a/music_assistant/client/config.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Handle Config related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, cast - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - CoreConfig, - PlayerConfig, - ProviderConfig, -) -from music_assistant.common.models.enums import ProviderType - -if TYPE_CHECKING: - from .client import MusicAssistantClient - - -class Config: - """Config related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - - # Provider Config related commands/functions - - async def get_provider_configs( - self, - provider_type: ProviderType | None = None, - provider_domain: str | None = None, - include_values: bool = False, - ) -> list[ProviderConfig]: - """Return all known provider configurations, optionally filtered by ProviderType.""" - return [ - ProviderConfig.from_dict(item) - for item in await self.client.send_command( - "config/providers", - provider_type=provider_type, - provider_domain=provider_domain, - include_values=include_values, - ) - ] - - async def get_provider_config(self, instance_id: str) -> ProviderConfig: - """Return (full) configuration for a single provider.""" - return ProviderConfig.from_dict( - await self.client.send_command("config/providers/get", instance_id=instance_id) - ) - - async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueType: - """Return single configentry value for a provider.""" - return cast( - ConfigValueType, - await self.client.send_command( - "config/providers/get_value", instance_id=instance_id, key=key - ), - ) - - async def get_provider_config_entries( - self, - provider_domain: str, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup/configure a provider. - - provider_domain: (mandatory) domain of the provider. - instance_id: id of an existing provider instance (None for new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return tuple( - ConfigEntry.from_dict(x) - for x in await self.client.send_command( - "config/providers/get_entries", - provider_domain=provider_domain, - instance_id=instance_id, - action=action, - values=values, - ) - ) - - async def save_provider_config( - self, - provider_domain: str, - values: dict[str, ConfigValueType], - instance_id: str | None = None, - ) -> ProviderConfig: - """ - Save Provider(instance) Config. - - provider_domain: (mandatory) domain of the provider. - values: the raw values for config entries that need to be stored/updated. - instance_id: id of an existing provider instance (None for new instance setup). - """ - return ProviderConfig.from_dict( - await self.client.send_command( - "config/providers/save", - provider_domain=provider_domain, - values=values, - instance_id=instance_id, - ) - ) - - async def remove_provider_config(self, instance_id: str) -> None: - """Remove ProviderConfig.""" - await self.client.send_command( - "config/providers/remove", - instance_id=instance_id, - ) - - async def reload_provider(self, instance_id: str) -> None: - """Reload provider.""" - await self.client.send_command( - "config/providers/reload", - instance_id=instance_id, - ) - - # Player Config related commands/functions - - async def get_player_configs( - self, provider: str | None = None, include_values: bool = False - ) -> list[PlayerConfig]: - """Return all known player configurations, optionally filtered by provider domain.""" - return [ - PlayerConfig.from_dict(item) - for item in await self.client.send_command( - "config/players", - provider=provider, - include_values=include_values, - ) - ] - - async def get_player_config(self, player_id: str) -> PlayerConfig: - """Return (full) configuration for a single player.""" - return PlayerConfig.from_dict( - await self.client.send_command("config/players/get", player_id=player_id) - ) - - async def get_player_config_value( - self, - player_id: str, - key: str, - ) -> ConfigValueType: - """Return single configentry value for a player.""" - return cast( - ConfigValueType, - await self.client.send_command( - "config/players/get_value", player_id=player_id, key=key - ), - ) - - async def save_player_config( - self, player_id: str, values: dict[str, ConfigValueType] - ) -> PlayerConfig: - """Save/update PlayerConfig.""" - return PlayerConfig.from_dict( - await self.client.send_command( - "config/players/save", player_id=player_id, values=values - ) - ) - - async def remove_player_config(self, player_id: str) -> None: - """Remove PlayerConfig.""" - await self.client.send_command("config/players/remove", player_id=player_id) - - # Core Controller config commands - - async def get_core_configs(self, include_values: bool = False) -> list[CoreConfig]: - """Return all core controllers config options.""" - return [ - CoreConfig.from_dict(item) - for item in await self.client.send_command( - "config/core", - include_values=include_values, - ) - ] - - async def get_core_config(self, domain: str) -> CoreConfig: - """Return configuration for a single core controller.""" - return CoreConfig.from_dict( - await self.client.send_command( - "config/core/get", - domain=domain, - ) - ) - - async def get_core_config_value(self, domain: str, key: str) -> ConfigValueType: - """Return single configentry value for a core controller.""" - return cast( - ConfigValueType, - await self.client.send_command("config/core/get_value", domain=domain, key=key), - ) - - async def get_core_config_entries( - self, - domain: str, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to configure a core controller. - - core_controller: name of the core controller - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return tuple( - ConfigEntry.from_dict(x) - for x in await self.client.send_command( - "config/core/get_entries", - domain=domain, - action=action, - values=values, - ) - ) - - async def save_core_config( - self, - domain: str, - values: dict[str, ConfigValueType], - ) -> CoreConfig: - """Save CoreController Config values.""" - return CoreConfig.from_dict( - await self.client.send_command( - "config/core/get_entries", - domain=domain, - values=values, - ) - ) diff --git a/music_assistant/client/connection.py b/music_assistant/client/connection.py deleted file mode 100644 index 9d7597a2..00000000 --- a/music_assistant/client/connection.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Connect o a remote Music Assistant Server using the default Websocket API.""" - -from __future__ import annotations - -import logging -import pprint -from typing import Any, cast - -from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType, client_exceptions - -from music_assistant.client.exceptions import ( - CannotConnect, - ConnectionClosed, - ConnectionFailed, - InvalidMessage, - InvalidState, - NotConnected, -) -from music_assistant.common.helpers.json import json_dumps, json_loads - -LOGGER = logging.getLogger(f"{__package__}.connection") - - -def get_websocket_url(url: str) -> str: - """Extract Websocket URL from (base) Music Assistant URL.""" - if not url or "://" not in url: - msg = f"{url} is not a valid url" - raise RuntimeError(msg) - ws_url = url.replace("http", "ws") - if not ws_url.endswith("/ws"): - ws_url += "/ws" - return ws_url.replace("//ws", "/ws") - - -class WebsocketsConnection: - """Websockets connection to a Music Assistant Server.""" - - def __init__(self, server_url: str, aiohttp_session: ClientSession | None) -> None: - """Initialize.""" - self.ws_server_url = get_websocket_url(server_url) - self._aiohttp_session_provided = aiohttp_session is not None - self._aiohttp_session: ClientSession | None = aiohttp_session or ClientSession() - self._ws_client: ClientWebSocketResponse | None = None - - @property - def connected(self) -> bool: - """Return if we're currently connected.""" - return self._ws_client is not None and not self._ws_client.closed - - async def connect(self) -> dict[str, Any]: - """Connect to the websocket server and return the first message (server info).""" - if self._aiohttp_session is None: - self._aiohttp_session = ClientSession() - if self._ws_client is not None: - msg = "Already connected" - raise InvalidState(msg) - - LOGGER.debug("Trying to connect") - try: - self._ws_client = await self._aiohttp_session.ws_connect( - self.ws_server_url, - heartbeat=55, - compress=15, - max_msg_size=0, - ) - # receive first server info message - return await self.receive_message() - except ( - client_exceptions.WSServerHandshakeError, - client_exceptions.ClientError, - ) as err: - raise CannotConnect(err) from err - - async def disconnect(self) -> None: - """Disconnect the client.""" - LOGGER.debug("Closing client connection") - if self._ws_client is not None and not self._ws_client.closed: - await self._ws_client.close() - self._ws_client = None - if self._aiohttp_session and not self._aiohttp_session_provided: - await self._aiohttp_session.close() - self._aiohttp_session = None - - async def receive_message(self) -> dict[str, Any]: - """Receive the next message from the server (or raise on error).""" - assert self._ws_client - ws_msg = await self._ws_client.receive() - - if ws_msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): - raise ConnectionClosed("Connection was closed.") - - if ws_msg.type == WSMsgType.ERROR: - raise ConnectionFailed - - if ws_msg.type != WSMsgType.TEXT: - raise InvalidMessage(f"Received non-Text message: {ws_msg.type}") - - try: - msg = cast(dict[str, Any], json_loads(ws_msg.data)) - except TypeError as err: - raise InvalidMessage(f"Received unsupported JSON: {err}") from err - except ValueError as err: - raise InvalidMessage("Received invalid JSON.") from err - - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("Received message:\n%s\n", pprint.pformat(ws_msg)) - - return msg - - async def send_message(self, message: dict[str, Any]) -> None: - """ - Send a message to the server. - - Raises NotConnected if client not connected. - """ - if not self.connected: - raise NotConnected - - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("Publishing message:\n%s\n", pprint.pformat(message)) - - assert self._ws_client - assert isinstance(message, dict) - - await self._ws_client.send_json(message, dumps=json_dumps) - - def __repr__(self) -> str: - """Return the representation.""" - prefix = "" if self.connected else "not " - return f"{type(self).__name__}(ws_server_url={self.ws_server_url!r}, {prefix}connected)" diff --git a/music_assistant/client/exceptions.py b/music_assistant/client/exceptions.py deleted file mode 100644 index fb1349c3..00000000 --- a/music_assistant/client/exceptions.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Client-specific Exceptions for Music Assistant.""" - -from __future__ import annotations - - -class MusicAssistantClientException(Exception): - """Generic MusicAssistant exception.""" - - -class TransportError(MusicAssistantClientException): - """Exception raised to represent transport errors.""" - - def __init__(self, message: str, error: Exception | None = None) -> None: - """Initialize a transport error.""" - super().__init__(message) - self.error = error - - -class ConnectionClosed(TransportError): - """Exception raised when the connection is closed.""" - - -class CannotConnect(TransportError): - """Exception raised when failed to connect the client.""" - - def __init__(self, error: Exception) -> None: - """Initialize a cannot connect error.""" - super().__init__(f"{error}", error) - - -class ConnectionFailed(TransportError): - """Exception raised when an established connection fails.""" - - def __init__(self, error: Exception | None = None) -> None: - """Initialize a connection failed error.""" - if error is None: - super().__init__("Connection failed.") - return - super().__init__(f"{error}", error) - - -class NotConnected(MusicAssistantClientException): - """Exception raised when not connected to client.""" - - -class InvalidState(MusicAssistantClientException): - """Exception raised when data gets in invalid state.""" - - -class InvalidMessage(MusicAssistantClientException): - """Exception raised when an invalid message is received.""" - - -class InvalidServerVersion(MusicAssistantClientException): - """Exception raised when connected to server with incompatible version.""" diff --git a/music_assistant/client/music.py b/music_assistant/client/music.py deleted file mode 100644 index 797c10a5..00000000 --- a/music_assistant/client/music.py +++ /dev/null @@ -1,570 +0,0 @@ -"""Handle Music/library related endpoints for Music Assistant.""" - -from __future__ import annotations - -import urllib.parse -from typing import TYPE_CHECKING, cast - -from music_assistant.common.models.enums import AlbumType, ImageType, MediaType -from music_assistant.common.models.media_items import ( - Album, - Artist, - ItemMapping, - MediaItemImage, - MediaItemMetadata, - MediaItemType, - Playlist, - PlaylistTrack, - Radio, - SearchResults, - Track, - media_from_dict, -) -from music_assistant.common.models.provider import SyncTask -from music_assistant.common.models.queue_item import QueueItem - -if TYPE_CHECKING: - from .client import MusicAssistantClient - - -class Music: - """Music(library) related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - - # Tracks related endpoints/commands - - async def get_library_tracks( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Track]: - """Get Track listing from the server.""" - return [ - Track.from_dict(obj) - for obj in await self.client.send_command( - "music/tracks/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_track( - self, - item_id: str, - provider_instance_id_or_domain: str, - album_uri: str | None = None, - ) -> Track: - """Get single Track from the server.""" - return Track.from_dict( - await self.client.send_command( - "music/tracks/get_track", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - album_uri=album_uri, - ), - ) - - async def get_track_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Track]: - """Get all other versions for given Track from the server.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/tracks/track_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - async def get_track_albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Album]: - """Get all (known) albums this track is featured on.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/tracks/track_albums", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - def get_track_preview_url( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> str: - """Get URL to preview clip of given track.""" - assert self.client.server_info - encoded_url = urllib.parse.quote(urllib.parse.quote(item_id)) - return f"{self.client.server_info.base_url}/preview?path={encoded_url}&provider={provider_instance_id_or_domain}" # noqa: E501 - - # Albums related endpoints/commands - - async def get_library_albums( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - album_types: list[AlbumType] | None = None, - ) -> list[Album]: - """Get Albums listing from the server.""" - return [ - Album.from_dict(obj) - for obj in await self.client.send_command( - "music/albums/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - album_types=album_types, - ) - ] - - async def get_album( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Album: - """Get single Album from the server.""" - return Album.from_dict( - await self.client.send_command( - "music/albums/get_album", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_album_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Track]: - """Get tracks for given album.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/albums/album_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - async def get_album_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Album]: - """Get all other versions for given Album from the server.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/albums/album_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - # Artist related endpoints/commands - - async def get_library_artists( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - album_artists_only: bool = False, - ) -> list[Artist]: - """Get Artists listing from the server.""" - return [ - Artist.from_dict(obj) - for obj in await self.client.send_command( - "music/artists/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - album_artists_only=album_artists_only, - ) - ] - - async def get_artist( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Artist: - """Get single Artist from the server.""" - return Artist.from_dict( - await self.client.send_command( - "music/artists/get_artist", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_artist_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Track]: - """Get (top)tracks for given artist.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/artists/artist_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - async def get_artist_albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Album]: - """Get (top)albums for given artist.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/artists/artist_albums", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - # Playlist related endpoints/commands - - async def get_library_playlists( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Playlist]: - """Get Playlists listing from the server.""" - return [ - Playlist.from_dict(obj) - for obj in await self.client.send_command( - "music/playlists/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_playlist( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Playlist: - """Get single Playlist from the server.""" - return Playlist.from_dict( - await self.client.send_command( - "music/playlists/get_playlist", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_playlist_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - page: int = 0, - ) -> list[PlaylistTrack]: - """Get tracks for given playlist.""" - return [ - PlaylistTrack.from_dict(obj) - for obj in await self.client.send_command( - "music/playlists/playlist_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - page=page, - ) - ] - - async def add_playlist_tracks(self, db_playlist_id: str | int, uris: list[str]) -> None: - """Add multiple tracks to playlist. Creates background tasks to process the action.""" - await self.client.send_command( - "music/playlists/add_playlist_tracks", - db_playlist_id=db_playlist_id, - uris=uris, - ) - - async def remove_playlist_tracks( - self, db_playlist_id: str | int, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove multiple tracks from playlist.""" - await self.client.send_command( - "music/playlists/remove_playlist_tracks", - db_playlist_id=db_playlist_id, - positions_to_remove=positions_to_remove, - ) - - async def create_playlist( - self, name: str, provider_instance_or_domain: str | None = None - ) -> Playlist: - """Create new playlist.""" - return Playlist.from_dict( - await self.client.send_command( - "music/playlists/create_playlist", - name=name, - provider_instance_or_domain=provider_instance_or_domain, - ) - ) - - # Radio related endpoints/commands - - async def get_library_radios( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Radio]: - """Get Radio listing from the server.""" - return [ - Radio.from_dict(obj) - for obj in await self.client.send_command( - "music/radios/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_radio( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Radio: - """Get single Radio from the server.""" - return Radio.from_dict( - await self.client.send_command( - "music/radios/get_radio", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_radio_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Radio]: - """Get all other versions for given Radio from the server.""" - return [ - Radio.from_dict(item) - for item in await self.client.send_command( - "music/radios/radio_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - # Other/generic endpoints/commands - - async def start_sync( - self, - media_types: list[MediaType] | None = None, - providers: list[str] | None = None, - ) -> None: - """Start running the sync of (all or selected) musicproviders. - - media_types: only sync these media types. None for all. - providers: only sync these provider instances. None for all. - """ - await self.client.send_command("music/sync", media_types=media_types, providers=providers) - - async def get_running_sync_tasks(self) -> list[SyncTask]: - """Return list with providers that are currently (scheduled for) syncing.""" - return [SyncTask(**item) for item in await self.client.send_command("music/synctasks")] - - async def search( - self, - search_query: str, - media_types: list[MediaType] = MediaType.ALL, - limit: int = 50, - library_only: bool = False, - ) -> SearchResults: - """Perform global search for media items on all providers. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: number of items to return in the search (per type). - """ - return SearchResults.from_dict( - await self.client.send_command( - "music/search", - search_query=search_query, - media_types=media_types, - limit=limit, - library_only=library_only, - ), - ) - - async def browse( - self, - path: str | None = None, - ) -> list[MediaItemType | ItemMapping]: - """Browse Music providers.""" - return [ - media_from_dict(obj) - for obj in await self.client.send_command("music/browse", path=path) - ] - - async def recently_played( - self, limit: int = 10, media_types: list[MediaType] | None = None - ) -> list[MediaItemType | ItemMapping]: - """Return a list of the last played items.""" - return [ - media_from_dict(item) - for item in await self.client.send_command( - "music/recently_played_items", limit=limit, media_types=media_types - ) - ] - - async def get_item_by_uri( - self, - uri: str, - ) -> MediaItemType | ItemMapping: - """Get single music item providing a mediaitem uri.""" - return media_from_dict(await self.client.send_command("music/item_by_uri", uri=uri)) - - async def get_item( - self, - media_type: MediaType, - item_id: str, - provider_instance_id_or_domain: str, - ) -> MediaItemType | ItemMapping: - """Get single music item by id and media type.""" - return media_from_dict( - await self.client.send_command( - "music/item", - media_type=media_type, - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ) - - async def add_item_to_favorites( - self, - item: str | MediaItemType, - ) -> None: - """Add an item to the favorites.""" - await self.client.send_command("music/favorites/add_item", item=item) - - async def remove_item_from_favorites( - self, - media_type: MediaType, - item_id: str | int, - ) -> None: - """Remove (library) item from the favorites.""" - await self.client.send_command( - "music/favorites/remove_item", - media_type=media_type, - item_id=item_id, - ) - - async def remove_item_from_library( - self, media_type: MediaType, library_item_id: str | int - ) -> None: - """ - Remove item from the library. - - Destructive! Will remove the item and all dependants. - """ - await self.client.send_command( - "music/library/remove_item", - media_type=media_type, - library_item_id=library_item_id, - ) - - async def add_item_to_library( - self, item: str | MediaItemType, overwrite_existing: bool = False - ) -> MediaItemType: - """Add item (uri or mediaitem) to the library.""" - return cast( - MediaItemType, - await self.client.send_command( - "music/library/add_item", item=item, overwrite_existing=overwrite_existing - ), - ) - - async def refresh_item( - self, - media_item: MediaItemType, - ) -> MediaItemType | ItemMapping | None: - """Try to refresh a mediaitem by requesting it's full object or search for substitutes.""" - if result := await self.client.send_command("music/refresh_item", media_item=media_item): - return media_from_dict(result) - return None - - # helpers - - def get_media_item_image( - self, - item: MediaItemType | ItemMapping | QueueItem, - type: ImageType = ImageType.THUMB, # noqa: A002 - ) -> MediaItemImage | None: - """Get MediaItemImage for MediaItem, ItemMapping.""" - if not item: - # guard for unexpected bad things - return None - # handle image in itemmapping - if item.image and item.image.type == type: - return item.image - # always prefer album image for tracks - album: Album | ItemMapping | None - if album := getattr(item, "album", None): - if album_image := self.get_media_item_image(album, type): - return album_image - # handle regular image within mediaitem - metadata: MediaItemMetadata | None - if metadata := getattr(item, "metadata", None): - for img in metadata.images or []: - if img.type == type: - return cast(MediaItemImage, img) - # retry with album/track artist(s) - artists: list[Artist | ItemMapping] | None - if artists := getattr(item, "artists", None): - for artist in artists: - if artist_image := self.get_media_item_image(artist, type): - return artist_image - # allow landscape fallback - if type == ImageType.THUMB: - return self.get_media_item_image(item, ImageType.LANDSCAPE) - return None diff --git a/music_assistant/client/player_queues.py b/music_assistant/client/player_queues.py deleted file mode 100644 index a52bf692..00000000 --- a/music_assistant/client/player_queues.py +++ /dev/null @@ -1,251 +0,0 @@ -"""Handle PlayerQueues related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import EventType, QueueOption, RepeatMode -from music_assistant.common.models.player_queue import PlayerQueue -from music_assistant.common.models.queue_item import QueueItem - -if TYPE_CHECKING: - from collections.abc import Iterator - - from music_assistant.common.models.event import MassEvent - from music_assistant.common.models.media_items import MediaItemType - - from .client import MusicAssistantClient - - -class PlayerQueues: - """PlayerQueue related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - # subscribe to player events - client.subscribe( - self._handle_event, - ( - EventType.QUEUE_ADDED, - EventType.QUEUE_UPDATED, - ), - ) - # the initial items are retrieved after connect - self._queues: dict[str, PlayerQueue] = {} - - @property - def player_queues(self) -> list[PlayerQueue]: - """Return all player queues.""" - return list(self._queues.values()) - - def __iter__(self) -> Iterator[PlayerQueue]: - """Iterate over (available) PlayerQueues.""" - return iter(self._queues.values()) - - def get(self, queue_id: str) -> PlayerQueue | None: - """Return PlayerQueue by ID (or None if not found).""" - return self._queues.get(queue_id) - - # PlayerQueue related endpoints/commands - - async def get_player_queue_items( - self, queue_id: str, limit: int = 500, offset: int = 0 - ) -> list[QueueItem]: - """Get all QueueItems for given PlayerQueue.""" - return [ - QueueItem.from_dict(obj) - for obj in await self.client.send_command( - "player_queues/items", queue_id=queue_id, limit=limit, offset=offset - ) - ] - - async def get_active_queue(self, player_id: str) -> PlayerQueue: - """Return the current active/synced queue for a player.""" - return PlayerQueue.from_dict( - await self.client.send_command("player_queues/get_active_queue", player_id=player_id) - ) - - async def queue_command_play(self, queue_id: str) -> None: - """Send PLAY command to given queue.""" - await self.client.send_command("player_queues/play", queue_id=queue_id) - - async def queue_command_pause(self, queue_id: str) -> None: - """Send PAUSE command to given queue.""" - await self.client.send_command("player_queues/pause", queue_id=queue_id) - - async def queue_command_stop(self, queue_id: str) -> None: - """Send STOP command to given queue.""" - await self.client.send_command("player_queues/stop", queue_id=queue_id) - - async def queue_command_resume(self, queue_id: str, fade_in: bool | None = None) -> None: - """Handle RESUME command for given queue. - - - queue_id: queue_id of the queue to handle the command. - """ - await self.client.send_command("player_queues/resume", queue_id=queue_id, fade_in=fade_in) - - async def queue_command_next(self, queue_id: str) -> None: - """Send NEXT TRACK command to given queue.""" - await self.client.send_command("player_queues/next", queue_id=queue_id) - - async def queue_command_previous(self, queue_id: str) -> None: - """Send PREVIOUS TRACK command to given queue.""" - await self.client.send_command("player_queues/previous", queue_id=queue_id) - - async def queue_command_clear(self, queue_id: str) -> None: - """Send CLEAR QUEUE command to given queue.""" - await self.client.send_command("player_queues/clear", queue_id=queue_id) - - async def queue_command_move_item( - self, queue_id: str, queue_item_id: str, pos_shift: int = 1 - ) -> None: - """ - Move queue item x up/down the queue. - - Parameters: - - queue_id: id of the queue to process this request. - - queue_item_id: the item_id of the queueitem that needs to be moved. - - pos_shift: move item x positions down if positive value - - pos_shift: move item x positions up if negative value - - pos_shift: move item to top of queue as next item if 0 - - NOTE: Fails if the given QueueItem is already playing or loaded in the buffer. - """ - await self.client.send_command( - "player_queues/move_item", - queue_id=queue_id, - queue_item_id=queue_item_id, - pos_shift=pos_shift, - ) - - async def queue_command_move_up(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item one place up in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=-1 - ) - - async def queue_command_move_down(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item one place down in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=1 - ) - - async def queue_command_move_next(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item as next up in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=0 - ) - - async def queue_command_delete(self, queue_id: str, item_id_or_index: int | str) -> None: - """Delete item (by id or index) from the queue.""" - await self.client.send_command( - "player_queues/delete_item", queue_id=queue_id, item_id_or_index=item_id_or_index - ) - - async def queue_command_seek(self, queue_id: str, position: int) -> None: - """ - Handle SEEK command for given queue. - - Parameters: - - position: position in seconds to seek to in the current playing item. - """ - await self.client.send_command("player_queues/seek", queue_id=queue_id, position=position) - - async def queue_command_skip(self, queue_id: str, seconds: int) -> None: - """ - Handle SKIP command for given queue. - - Parameters: - - seconds: number of seconds to skip in track. Use negative value to skip back. - """ - await self.client.send_command("player_queues/skip", queue_id=queue_id, seconds=seconds) - - async def queue_command_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None: - """Configure shuffle mode on the the queue.""" - await self.client.send_command( - "player_queues/shuffle", queue_id=queue_id, shuffle_enabled=shuffle_enabled - ) - - async def queue_command_repeat(self, queue_id: str, repeat_mode: RepeatMode) -> None: - """Configure repeat mode on the the queue.""" - await self.client.send_command( - "player_queues/repeat", queue_id=queue_id, repeat_mode=repeat_mode - ) - - async def play_index( - self, - queue_id: str, - index: int | str, - seek_position: int = 0, - fade_in: bool = False, - ) -> None: - """Play item at index (or item_id) X in queue.""" - await self.client.send_command( - "player_queues/repeat", - queue_id=queue_id, - index=index, - seek_position=seek_position, - fade_in=fade_in, - ) - - async def play_media( - self, - queue_id: str, - media: MediaItemType | list[MediaItemType] | str | list[str], - option: QueueOption | None = None, - radio_mode: bool = False, - start_item: str | None = None, - ) -> None: - """ - Play media item(s) on the given queue. - - - media: Media that should be played (MediaItem(s) or uri's). - - queue_opt: Which enqueue mode to use. - - radio_mode: Enable radio mode for the given item(s). - - start_item: Optional item to start the playlist or album from. - """ - await self.client.send_command( - "player_queues/play_media", - queue_id=queue_id, - media=media, - option=option, - radio_mode=radio_mode, - start_item=start_item, - ) - - async def transfer_queue( - self, - source_queue_id: str, - target_queue_id: str, - auto_play: bool | None = None, - ) -> None: - """Transfer queue to another queue.""" - await self.client.send_command( - "player_queues/transfer", - source_queue_id=source_queue_id, - target_queue_id=target_queue_id, - auto_play=auto_play, - require_schema=25, - ) - - # Other endpoints/commands - - async def _get_player_queues(self) -> list[PlayerQueue]: - """Fetch all PlayerQueues from the server.""" - return [ - PlayerQueue.from_dict(item) - for item in await self.client.send_command("player_queues/all") - ] - - async def fetch_state(self) -> None: - """Fetch initial state once the server is connected.""" - for queue in await self._get_player_queues(): - self._queues[queue.queue_id] = queue - - def _handle_event(self, event: MassEvent) -> None: - """Handle incoming player(queue) event.""" - if event.event in (EventType.QUEUE_ADDED, EventType.QUEUE_UPDATED): - # Queue events always have an object_id - assert event.object_id - self._queues[event.object_id] = PlayerQueue.from_dict(event.data) diff --git a/music_assistant/client/players.py b/music_assistant/client/players.py deleted file mode 100644 index 3173caba..00000000 --- a/music_assistant/client/players.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Handle player related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.player import Player - -if TYPE_CHECKING: - from collections.abc import Iterator - - from music_assistant.common.models.event import MassEvent - - from .client import MusicAssistantClient - - -class Players: - """Player related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - # subscribe to player events - client.subscribe( - self._handle_event, - ( - EventType.PLAYER_ADDED, - EventType.PLAYER_REMOVED, - EventType.PLAYER_UPDATED, - ), - ) - # the initial items are retrieved after connect - self._players: dict[str, Player] = {} - - @property - def players(self) -> list[Player]: - """Return all players.""" - return list(self._players.values()) - - def __iter__(self) -> Iterator[Player]: - """Iterate over (available) players.""" - return iter(self._players.values()) - - def get(self, player_id: str) -> Player | None: - """Return Player by ID (or None if not found).""" - return self._players.get(player_id) - - def __getitem__(self, player_id: str) -> Player: - """Return Player by ID.""" - return self._players[player_id] - - # Player related endpoints/commands - - async def player_command_stop(self, player_id: str) -> None: - """Send STOP command to given player (directly).""" - await self.client.send_command("players/cmd/stop", player_id=player_id) - - async def player_command_play(self, player_id: str) -> None: - """Send PLAY command to given player (directly).""" - await self.client.send_command("players/cmd/play", player_id=player_id) - - async def player_command_pause(self, player_id: str) -> None: - """Send PAUSE command to given player (directly).""" - await self.client.send_command("players/cmd/pause", player_id=player_id) - - async def player_command_play_pause(self, player_id: str) -> None: - """Send PLAY_PAUSE (toggle) command to given player (directly).""" - await self.client.send_command("players/cmd/pause", player_id=player_id) - - async def player_command_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player.""" - await self.client.send_command("players/cmd/power", player_id=player_id, powered=powered) - - async def player_command_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME SET command to given player.""" - await self.client.send_command( - "players/cmd/volume_set", player_id=player_id, volume_level=volume_level - ) - - async def player_command_volume_up(self, player_id: str) -> None: - """Send VOLUME UP command to given player.""" - await self.client.send_command("players/cmd/volume_up", player_id=player_id) - - async def player_command_volume_down(self, player_id: str) -> None: - """Send VOLUME DOWN command to given player.""" - await self.client.send_command("players/cmd/volume_down", player_id=player_id) - - async def player_command_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - await self.client.send_command("players/cmd/volume_mute", player_id=player_id, muted=muted) - - async def player_command_seek(self, player_id: str, position: int) -> None: - """Handle SEEK command for given player (directly). - - - player_id: player_id of the player to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - await self.client.send_command("players/cmd/seek", player_id=player_id, position=position) - - async def player_command_sync(self, player_id: str, target_player: str) -> None: - """ - Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - If the player is already synced to another player, it will be unsynced there first. - If the target player itself is already synced to another player, this will fail. - If the player can not be synced with the given target player, this will fail. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - await self.client.send_command( - "players/cmd/sync", player_id=player_id, target_player=target_player - ) - - async def player_command_unsync(self, player_id: str) -> None: - """ - Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - If the player is not currently synced to any other player, - this will silently be ignored. - - - player_id: player_id of the player to handle the command. - """ - await self.client.send_command("players/cmd/unsync", player_id=player_id) - - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - await self.client.send_command( - "players/cmd/sync_many", target_player=target_player, child_player_ids=child_player_ids - ) - - async def cmd_unsync_many(self, player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - await self.client.send_command("players/cmd/unsync_many", player_ids=player_ids) - - async def play_announcement( - self, - player_id: str, - url: str, - use_pre_announce: bool | None = None, - volume_level: int | None = None, - ) -> None: - """Handle playback of an announcement (url) on given player.""" - await self.client.send_command( - "players/cmd/play_announcement", - player_id=player_id, - url=url, - use_pre_announce=use_pre_announce, - volume_level=volume_level, - ) - - # PlayerGroup related endpoints/commands - - async def create_syncgroup(self, name: str, members: list[str]) -> Player: - """Create a new Sync Group with name and members. - - - name: Name for the new group to create. - - members: A list of player_id's that should be part of this group. - - Returns the newly created player on success. - """ - return Player.from_dict( - await self.client.send_command("players/create_syncgroup", name=name, members=members) - ) - - async def set_player_group_volume(self, player_id: str, volume_level: int) -> None: - """ - Send VOLUME_SET command to given playergroup. - - Will send the new (average) volume level to group child's. - - player_id: player_id of the playergroup to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - await self.client.send_command( - "players/cmd/group_volume", player_id=player_id, volume_level=volume_level - ) - - async def set_player_group_members(self, player_id: str, members: list[str]) -> None: - """ - Update the memberlist of the given PlayerGroup. - - - player_id: player_id of the groupplayer to handle the command. - - members: list of player ids to set as members. - """ - await self.client.send_command( - "players/cmd/set_members", player_id=player_id, members=members - ) - - # Other endpoints/commands - - async def _get_players(self) -> list[Player]: - """Fetch all Players from the server.""" - return [Player.from_dict(item) for item in await self.client.send_command("players/all")] - - async def fetch_state(self) -> None: - """Fetch initial state once the server is connected.""" - for player in await self._get_players(): - self._players[player.player_id] = player - - def _handle_event(self, event: MassEvent) -> None: - """Handle incoming player event.""" - if event.event in (EventType.PLAYER_ADDED, EventType.PLAYER_UPDATED): - # Player events always have an object id - assert event.object_id - self._players[event.object_id] = Player.from_dict(event.data) - return - if event.event == EventType.PLAYER_REMOVED: - # Player events always have an object id - assert event.object_id - self._players.pop(event.object_id, None) diff --git a/music_assistant/common/__init__.py b/music_assistant/common/__init__.py deleted file mode 100644 index c450cee2..00000000 --- a/music_assistant/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Provide common/shared files for the Music Assistant Server and client.""" diff --git a/music_assistant/common/helpers/__init__.py b/music_assistant/common/helpers/__init__.py deleted file mode 100644 index 65147294..00000000 --- a/music_assistant/common/helpers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Various utils/helpers.""" diff --git a/music_assistant/common/helpers/datetime.py b/music_assistant/common/helpers/datetime.py deleted file mode 100644 index af8f4b24..00000000 --- a/music_assistant/common/helpers/datetime.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Helpers for date and time.""" - -from __future__ import annotations - -import datetime - -LOCAL_TIMEZONE = datetime.datetime.now(datetime.UTC).astimezone().tzinfo - - -def utc() -> datetime.datetime: - """Get current UTC datetime.""" - return datetime.datetime.now(datetime.UTC) - - -def utc_timestamp() -> float: - """Return UTC timestamp in seconds as float.""" - return utc().timestamp() - - -def now() -> datetime.datetime: - """Get current datetime in local timezone.""" - return datetime.datetime.now(LOCAL_TIMEZONE) - - -def now_timestamp() -> float: - """Return current datetime as timestamp in local timezone.""" - return now().timestamp() - - -def future_timestamp(**kwargs: float) -> float: - """Return current timestamp + timedelta.""" - return (now() + datetime.timedelta(**kwargs)).timestamp() - - -def from_utc_timestamp(timestamp: float) -> datetime.datetime: - """Return datetime from UTC timestamp.""" - return datetime.datetime.fromtimestamp(timestamp, datetime.UTC) - - -def iso_from_utc_timestamp(timestamp: float) -> str: - """Return ISO 8601 datetime string from UTC timestamp.""" - return from_utc_timestamp(timestamp).isoformat() - - -def from_iso_string(iso_datetime: str) -> datetime.datetime: - """Return datetime from ISO datetime string.""" - return datetime.datetime.fromisoformat(iso_datetime) diff --git a/music_assistant/common/helpers/global_cache.py b/music_assistant/common/helpers/global_cache.py deleted file mode 100644 index 6cd741dd..00000000 --- a/music_assistant/common/helpers/global_cache.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Provides a simple global memory cache.""" - -from __future__ import annotations - -import asyncio -from typing import Any - -# global cache - we use this on a few places (as limited as possible) -# where we have no other options -_global_cache_lock = asyncio.Lock() -_global_cache: dict[str, Any] = {} - - -def get_global_cache_value(key: str, default: Any = None) -> Any: - """Get a value from the global cache.""" - return _global_cache.get(key, default) - - -async def set_global_cache_values(values: dict[str, Any]) -> Any: - """Set a value in the global cache (without locking).""" - async with _global_cache_lock: - for key, value in values.items(): - _set_global_cache_value(key, value) - - -def _set_global_cache_value(key: str, value: Any) -> Any: - """Set a value in the global cache (without locking).""" - _global_cache[key] = value diff --git a/music_assistant/common/helpers/json.py b/music_assistant/common/helpers/json.py deleted file mode 100644 index 49b85ba7..00000000 --- a/music_assistant/common/helpers/json.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Helpers to work with (de)serializing of json.""" - -import asyncio -import base64 -from _collections_abc import dict_keys, dict_values -from types import MethodType -from typing import Any, TypeVar - -import aiofiles -import orjson -from mashumaro.mixins.orjson import DataClassORJSONMixin - -JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) -JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,) - -DO_NOT_SERIALIZE_TYPES = (MethodType, asyncio.Task) - - -def get_serializable_value(obj: Any, raise_unhandled: bool = False) -> Any: - """Parse the value to its serializable equivalent.""" - if getattr(obj, "do_not_serialize", None): - return None - if ( - isinstance(obj, list | set | filter | tuple | dict_values | dict_keys | dict_values) - or obj.__class__ == "dict_valueiterator" - ): - return [get_serializable_value(x) for x in obj] - if hasattr(obj, "to_dict"): - return obj.to_dict() - if isinstance(obj, bytes): - return base64.b64encode(obj).decode("ascii") - if isinstance(obj, DO_NOT_SERIALIZE_TYPES): - return None - if raise_unhandled: - raise TypeError - return obj - - -def serialize_to_json(obj: Any) -> Any: - """Serialize a value (or a list of values) to json.""" - if obj is None: - return obj - if hasattr(obj, "to_json"): - return obj.to_json() - return json_dumps(get_serializable_value(obj)) - - -def json_dumps(data: Any, indent: bool = False) -> str: - """Dump json string.""" - # we use the passthrough dataclass option because we use mashumaro for that - option = orjson.OPT_OMIT_MICROSECONDS | orjson.OPT_PASSTHROUGH_DATACLASS - if indent: - option |= orjson.OPT_INDENT_2 - return orjson.dumps( - data, - default=get_serializable_value, - option=option, - ).decode("utf-8") - - -json_loads = orjson.loads - -TargetT = TypeVar("TargetT", bound=DataClassORJSONMixin) - - -async def load_json_file(path: str, target_class: type[TargetT]) -> TargetT: - """Load JSON from file.""" - async with aiofiles.open(path) as _file: - content = await _file.read() - return target_class.from_json(content) diff --git a/music_assistant/common/helpers/uri.py b/music_assistant/common/helpers/uri.py deleted file mode 100644 index a381861d..00000000 --- a/music_assistant/common/helpers/uri.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Helpers for creating/parsing URI's.""" - -import asyncio -import os -import re - -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.errors import InvalidProviderID, InvalidProviderURI - -base62_length22_id_pattern = re.compile(r"^[a-zA-Z0-9]{22}$") - - -def valid_base62_length22(item_id: str) -> bool: - """Validate Spotify style ID.""" - return bool(base62_length22_id_pattern.match(item_id)) - - -def valid_id(provider: str, item_id: str) -> bool: - """Validate Provider ID.""" - if provider == "spotify": - return valid_base62_length22(item_id) - else: - return True - - -async def parse_uri(uri: str, validate_id: bool = False) -> tuple[MediaType, str, str]: - """Try to parse URI to Mass identifiers. - - Returns Tuple: MediaType, provider_instance_id_or_domain, item_id - """ - try: - if uri.startswith("https://open."): - # public share URL (e.g. Spotify or Qobuz, not sure about others) - # https://open.spotify.com/playlist/5lH9NjOeJvctAO92ZrKQNB?si=04a63c8234ac413e - provider_instance_id_or_domain = uri.split(".")[1] - media_type_str = uri.split("/")[3] - media_type = MediaType(media_type_str) - item_id = uri.split("/")[4].split("?")[0] - elif uri.startswith("https://tidal.com/browse/"): - # Tidal public share URL - # https://tidal.com/browse/track/123456 - provider_instance_id_or_domain = "tidal" - media_type_str = uri.split("/")[4] - media_type = MediaType(media_type_str) - item_id = uri.split("/")[5].split("?")[0] - elif uri.startswith(("http://", "https://", "rtsp://", "rtmp://")): - # Translate a plain URL to the builtin provider - provider_instance_id_or_domain = "builtin" - media_type = MediaType.UNKNOWN - item_id = uri - elif "://" in uri and len(uri.split("/")) >= 4: - # music assistant-style uri - # provider://media_type/item_id - provider_instance_id_or_domain, rest = uri.split("://", 1) - media_type_str, item_id = rest.split("/", 1) - media_type = MediaType(media_type_str) - elif ":" in uri and len(uri.split(":")) == 3: - # spotify new-style uri - provider_instance_id_or_domain, media_type_str, item_id = uri.split(":") - media_type = MediaType(media_type_str) - elif "/" in uri and await asyncio.to_thread(os.path.isfile, uri): - # Translate a local file (which is not from a file provider!) to the builtin provider - provider_instance_id_or_domain = "builtin" - media_type = MediaType.UNKNOWN - item_id = uri - else: - raise KeyError - except (TypeError, AttributeError, ValueError, KeyError) as err: - msg = f"Not a valid Music Assistant uri: {uri}" - raise InvalidProviderURI(msg) from err - if validate_id and not valid_id(provider_instance_id_or_domain, item_id): - msg = f"Invalid {provider_instance_id_or_domain} ID: {item_id} found in URI: {uri}" - raise InvalidProviderID(msg) - return (media_type, provider_instance_id_or_domain, item_id) - - -def create_uri(media_type: MediaType, provider_instance_id_or_domain: str, item_id: str) -> str: - """Create Music Assistant URI from MediaItem values.""" - return f"{provider_instance_id_or_domain}://{media_type.value}/{item_id}" diff --git a/music_assistant/common/helpers/util.py b/music_assistant/common/helpers/util.py deleted file mode 100644 index 7c14e89e..00000000 --- a/music_assistant/common/helpers/util.py +++ /dev/null @@ -1,359 +0,0 @@ -"""Helper and utility functions.""" - -from __future__ import annotations - -import asyncio -import os -import re -import socket -from collections.abc import Callable -from collections.abc import Set as AbstractSet -from typing import Any, TypeVar -from urllib.parse import urlparse -from uuid import UUID - -T = TypeVar("T") -CALLBACK_TYPE = Callable[[], None] - -keyword_pattern = re.compile("title=|artist=") -title_pattern = re.compile(r"title=\"(?P.*?)\"") -artist_pattern = re.compile(r"artist=\"(?P<artist>.*?)\"") -dot_com_pattern = re.compile(r"(?P<netloc>\(?\w+\.(?:\w+\.)?(\w{2,3})\)?)") -ad_pattern = re.compile(r"((ad|advertisement)_)|^AD\s\d+$|ADBREAK", flags=re.IGNORECASE) -title_artist_order_pattern = re.compile(r"(?P<title>.+)\sBy:\s(?P<artist>.+)", flags=re.IGNORECASE) -multi_space_pattern = re.compile(r"\s{2,}") -end_junk_pattern = re.compile(r"(.+?)(\s\W+)$") - -VERSION_PARTS = ( - # list of common version strings - "version", - "live", - "edit", - "remix", - "mix", - "acoustic", - "instrumental", - "karaoke", - "remaster", - "versie", - "unplugged", - "disco", - "akoestisch", - "deluxe", -) -IGNORE_TITLE_PARTS = ( - # strings that may be stripped off a title part - # (most important the featuring parts) - "feat.", - "featuring", - "ft.", - "with ", - "explicit", -) - - -def filename_from_string(string: str) -> str: - """Create filename from unsafe string.""" - keepcharacters = (" ", ".", "_") - return "".join(c for c in string if c.isalnum() or c in keepcharacters).rstrip() - - -def try_parse_int(possible_int: Any, default: int | None = 0) -> int | None: - """Try to parse an int.""" - try: - return int(possible_int) - except (TypeError, ValueError): - return default - - -def try_parse_float(possible_float: Any, default: float | None = 0.0) -> float | None: - """Try to parse a float.""" - try: - return float(possible_float) - except (TypeError, ValueError): - return default - - -def try_parse_bool(possible_bool: Any) -> bool: - """Try to parse a bool.""" - if isinstance(possible_bool, bool): - return possible_bool - return possible_bool in ["true", "True", "1", "on", "ON", 1] - - -def try_parse_duration(duration_str: str) -> float: - """Try to parse a duration in seconds from a duration (HH:MM:SS) string.""" - milliseconds = float("0." + duration_str.split(".")[-1]) if "." in duration_str else 0.0 - duration_parts = duration_str.split(".")[0].split(",")[0].split(":") - if len(duration_parts) == 3: - seconds = sum(x * int(t) for x, t in zip([3600, 60, 1], duration_parts, strict=False)) - elif len(duration_parts) == 2: - seconds = sum(x * int(t) for x, t in zip([60, 1], duration_parts, strict=False)) - else: - seconds = int(duration_parts[0]) - return seconds + milliseconds - - -def create_sort_name(input_str: str) -> str: - """Create (basic/simple) sort name/title from string.""" - input_str = input_str.lower().strip() - for item in ["the ", "de ", "les ", "dj ", "las ", "los ", "le ", "la ", "el ", "a ", "an "]: - if input_str.startswith(item): - input_str = input_str.replace(item, "") + f", {item}" - return input_str.strip() - - -def parse_title_and_version(title: str, track_version: str | None = None) -> tuple[str, str]: - """Try to parse version from the title.""" - version = track_version or "" - for regex in (r"\(.*?\)", r"\[.*?\]", r" - .*"): - for title_part in re.findall(regex, title): - for ignore_str in IGNORE_TITLE_PARTS: - if ignore_str in title_part.lower(): - title = title.replace(title_part, "").strip() - continue - for version_str in VERSION_PARTS: - if version_str not in title_part.lower(): - continue - version = ( - title_part.replace("(", "") - .replace(")", "") - .replace("[", "") - .replace("]", "") - .replace("-", "") - .strip() - ) - title = title.replace(title_part, "").strip() - return (title, version) - return title, version - - -def strip_ads(line: str) -> str: - """Strip Ads from line.""" - if ad_pattern.search(line): - return "Advert" - return line - - -def strip_url(line: str) -> str: - """Strip URL from line.""" - return ( - " ".join([p for p in line.split() if (not urlparse(p).scheme or not urlparse(p).netloc)]) - ).rstrip() - - -def strip_dotcom(line: str) -> str: - """Strip scheme-less netloc from line.""" - return dot_com_pattern.sub("", line) - - -def strip_end_junk(line: str) -> str: - """Strip non-word info from end of line.""" - return end_junk_pattern.sub(r"\1", line) - - -def swap_title_artist_order(line: str) -> str: - """Swap title/artist order in line.""" - return title_artist_order_pattern.sub(r"\g<artist> - \g<title>", line) - - -def strip_multi_space(line: str) -> str: - """Strip multi-whitespace from line.""" - return multi_space_pattern.sub(" ", line) - - -def multi_strip(line: str) -> str: - """Strip assorted junk from line.""" - return strip_multi_space( - swap_title_artist_order(strip_end_junk(strip_dotcom(strip_url(strip_ads(line))))) - ).rstrip() - - -def clean_stream_title(line: str) -> str: - """Strip junk text from radio streamtitle.""" - title: str = "" - artist: str = "" - - if not keyword_pattern.search(line): - return multi_strip(line) - - if match := title_pattern.search(line): - title = multi_strip(match.group("title")) - - if match := artist_pattern.search(line): - possible_artist = multi_strip(match.group("artist")) - if possible_artist and possible_artist != title: - artist = possible_artist - - if not title and not artist: - return "" - - if title: - if re.search(" - ", title) or not artist: - return title - if artist: - return f"{artist} - {title}" - - if artist: - return artist - - return line - - -async def get_ip() -> str: - """Get primary IP-address for this host.""" - - def _get_ip() -> str: - """Get primary IP-address for this host.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - # doesn't even have to be reachable - sock.connect(("10.255.255.255", 1)) - _ip = str(sock.getsockname()[0]) - except Exception: - _ip = "127.0.0.1" - finally: - sock.close() - return _ip - - return await asyncio.to_thread(_get_ip) - - -async def is_port_in_use(port: int) -> bool: - """Check if port is in use.""" - - def _is_port_in_use() -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as _sock: - try: - _sock.bind(("0.0.0.0", port)) - except OSError: - return True - return False - - return await asyncio.to_thread(_is_port_in_use) - - -async def select_free_port(range_start: int, range_end: int) -> int: - """Automatically find available port within range.""" - for port in range(range_start, range_end): - if not await is_port_in_use(port): - return port - msg = "No free port available" - raise OSError(msg) - - -async def get_ip_from_host(dns_name: str) -> str | None: - """Resolve (first) IP-address for given dns name.""" - - def _resolve() -> str | None: - try: - return socket.gethostbyname(dns_name) - except Exception: - # fail gracefully! - return None - - return await asyncio.to_thread(_resolve) - - -async def get_ip_pton(ip_string: str | None = None) -> bytes: - """Return socket pton for local ip.""" - if ip_string is None: - ip_string = await get_ip() - try: - return await asyncio.to_thread(socket.inet_pton, socket.AF_INET, ip_string) - except OSError: - return await asyncio.to_thread(socket.inet_pton, socket.AF_INET6, ip_string) - - -def get_folder_size(folderpath: str) -> float: - """Return folder size in gb.""" - total_size = 0 - for dirpath, _dirnames, filenames in os.walk(folderpath): - for _file in filenames: - _fp = os.path.join(dirpath, _file) - total_size += os.path.getsize(_fp) - return total_size / float(1 << 30) - - -def merge_dict( - base_dict: dict[Any, Any], new_dict: dict[Any, Any], allow_overwite: bool = False -) -> dict[Any, Any]: - """Merge dict without overwriting existing values.""" - final_dict = base_dict.copy() - for key, value in new_dict.items(): - if final_dict.get(key) and isinstance(value, dict): - final_dict[key] = merge_dict(final_dict[key], value) - if final_dict.get(key) and isinstance(value, tuple): - final_dict[key] = merge_tuples(final_dict[key], value) - if final_dict.get(key) and isinstance(value, list): - final_dict[key] = merge_lists(final_dict[key], value) - elif not final_dict.get(key) or allow_overwite: - final_dict[key] = value - return final_dict - - -def merge_tuples(base: tuple[Any, ...], new: tuple[Any, ...]) -> tuple[Any, ...]: - """Merge 2 tuples.""" - return tuple(x for x in base if x not in new) + tuple(new) - - -def merge_lists(base: list[Any], new: list[Any]) -> list[Any]: - """Merge 2 lists.""" - return [x for x in base if x not in new] + list(new) - - -def get_changed_keys( - dict1: dict[str, Any], - dict2: dict[str, Any], - ignore_keys: list[str] | None = None, -) -> AbstractSet[str]: - """Compare 2 dicts and return set of changed keys.""" - return get_changed_values(dict1, dict2, ignore_keys).keys() - - -def get_changed_values( - dict1: dict[str, Any], - dict2: dict[str, Any], - ignore_keys: list[str] | None = None, -) -> dict[str, tuple[Any, Any]]: - """ - Compare 2 dicts and return dict of changed values. - - dict key is the changed key, value is tuple of old and new values. - """ - if not dict1 and not dict2: - return {} - if not dict1: - return {key: (None, value) for key, value in dict2.items()} - if not dict2: - return {key: (None, value) for key, value in dict1.items()} - changed_values = {} - for key, value in dict2.items(): - if ignore_keys and key in ignore_keys: - continue - if key not in dict1: - changed_values[key] = (None, value) - elif isinstance(value, dict): - changed_values.update(get_changed_values(dict1[key], value, ignore_keys)) - elif dict1[key] != value: - changed_values[key] = (dict1[key], value) - return changed_values - - -def empty_queue(q: asyncio.Queue[T]) -> None: - """Empty an asyncio Queue.""" - for _ in range(q.qsize()): - try: - q.get_nowait() - q.task_done() - except (asyncio.QueueEmpty, ValueError): - pass - - -def is_valid_uuid(uuid_to_test: str) -> bool: - """Check if uuid string is a valid UUID.""" - try: - uuid_obj = UUID(uuid_to_test) - except (ValueError, TypeError): - return False - return str(uuid_obj) == uuid_to_test diff --git a/music_assistant/common/models/__init__.py b/music_assistant/common/models/__init__.py deleted file mode 100644 index b10f7001..00000000 --- a/music_assistant/common/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package with all common/shared (serializable) Models (dataclassses).""" diff --git a/music_assistant/common/models/api.py b/music_assistant/common/models/api.py deleted file mode 100644 index ec43ea02..00000000 --- a/music_assistant/common/models/api.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Generic models used for the (websockets) API communication.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import get_serializable_value -from music_assistant.common.models.event import MassEvent - - -@dataclass -class CommandMessage(DataClassORJSONMixin): - """Model for a Message holding a command from server to client or client to server.""" - - message_id: str | int - command: str - args: dict[str, Any] | None = None - - -@dataclass -class ResultMessageBase(DataClassORJSONMixin): - """Base class for a result/response of a Command Message.""" - - message_id: str - - -@dataclass -class SuccessResultMessage(ResultMessageBase): - """Message sent when a Command has been successfully executed.""" - - result: Any = field(default=None, metadata={"serialize": lambda v: get_serializable_value(v)}) - partial: bool = False - - -@dataclass -class ErrorResultMessage(ResultMessageBase): - """Message sent when a command did not execute successfully.""" - - error_code: int - details: str | None = None - - -# EventMessage is the same as MassEvent, this is just a alias. -EventMessage = MassEvent - - -@dataclass -class ServerInfoMessage(DataClassORJSONMixin): - """Message sent by the server with it's info when a client connects.""" - - server_id: str - server_version: str - schema_version: int - min_supported_schema_version: int - base_url: str - homeassistant_addon: bool = False - onboard_done: bool = False - - -MessageType = ( - CommandMessage | EventMessage | SuccessResultMessage | ErrorResultMessage | ServerInfoMessage -) - - -def parse_message(raw: dict[Any, Any]) -> MessageType: - """Parse Message from raw dict object.""" - if "event" in raw: - return EventMessage.from_dict(raw) - if "error_code" in raw: - return ErrorResultMessage.from_dict(raw) - if "result" in raw: - return SuccessResultMessage.from_dict(raw) - if "sdk_version" in raw: - return ServerInfoMessage.from_dict(raw) - return CommandMessage.from_dict(raw) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py deleted file mode 100644 index 78480255..00000000 --- a/music_assistant/common/models/config_entries.py +++ /dev/null @@ -1,677 +0,0 @@ -"""Model and helpers for Config entries.""" - -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable, Iterable -from dataclasses import dataclass -from enum import Enum -from types import NoneType -from typing import Any - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.enums import ProviderType -from music_assistant.constants import ( - CONF_ANNOUNCE_VOLUME, - CONF_ANNOUNCE_VOLUME_MAX, - CONF_ANNOUNCE_VOLUME_MIN, - CONF_ANNOUNCE_VOLUME_STRATEGY, - CONF_AUTO_PLAY, - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_ENABLE_ICY_METADATA, - CONF_ENFORCE_MP3, - CONF_EQ_BASS, - CONF_EQ_MID, - CONF_EQ_TREBLE, - CONF_FLOW_MODE, - CONF_HIDE_PLAYER, - CONF_HTTP_PROFILE, - CONF_ICON, - CONF_LOG_LEVEL, - CONF_OUTPUT_CHANNELS, - CONF_SAMPLE_RATES, - CONF_SYNC_ADJUST, - CONF_TTS_PRE_ANNOUNCE, - CONF_VOLUME_NORMALIZATION, - CONF_VOLUME_NORMALIZATION_TARGET, - SECURE_STRING_SUBSTITUTE, -) - -from .enums import ConfigEntryType - -# TEMP: ignore UserWarnings from mashumaro -# https://github.com/Fatal1ty/mashumaro/issues/221 -warnings.filterwarnings("ignore", category=UserWarning, module="mashumaro") - -LOGGER = logging.getLogger(__name__) - -ENCRYPT_CALLBACK: Callable[[str], str] | None = None -DECRYPT_CALLBACK: Callable[[str], str] | None = None - -ConfigValueType = ( - str - | int - | float - | bool - | list[str] - | list[tuple[int, int]] - | tuple[int, int] - | list[int] - | Enum - | None -) - -ConfigEntryTypeMap: dict[ConfigEntryType, type[ConfigValueType]] = { - ConfigEntryType.BOOLEAN: bool, - ConfigEntryType.STRING: str, - ConfigEntryType.SECURE_STRING: str, - ConfigEntryType.INTEGER: int, - ConfigEntryType.INTEGER_TUPLE: tuple[int, int], - ConfigEntryType.FLOAT: float, - ConfigEntryType.LABEL: str, - ConfigEntryType.DIVIDER: str, - ConfigEntryType.ACTION: str, - ConfigEntryType.ALERT: str, - ConfigEntryType.ICON: str, -} - -UI_ONLY = ( - ConfigEntryType.LABEL, - ConfigEntryType.DIVIDER, - ConfigEntryType.ACTION, - ConfigEntryType.ALERT, -) - - -@dataclass -class ConfigValueOption(DataClassDictMixin): - """Model for a value with separated name/value.""" - - title: str - value: ConfigValueType - - -@dataclass -class ConfigEntry(DataClassDictMixin): - """Model for a Config Entry. - - The definition of something that can be configured - for an object (e.g. provider or player) - within Music Assistant. - """ - - # key: used as identifier for the entry, also for localization - key: str - type: ConfigEntryType - # label: default label when no translation for the key is present - label: str - default_value: ConfigValueType = None - required: bool = True - # options [optional]: select from list of possible values/options - options: tuple[ConfigValueOption, ...] | None = None - # range [optional]: select values within range - range: tuple[int, int] | None = None - # description [optional]: extended description of the setting. - description: str | None = None - # help_link [optional]: link to help article. - help_link: str | None = None - # multi_value [optional]: allow multiple values from the list - multi_value: bool = False - # depends_on [optional]: needs to be set before this setting shows up in frontend - depends_on: str | None = None - # hidden: hide from UI - hidden: bool = False - # category: category to group this setting into in the frontend (e.g. advanced) - category: str = "generic" - # action: (configentry)action that is needed to get the value for this entry - action: str | None = None - # action_label: default label for the action when no translation for the action is present - action_label: str | None = None - # value: set by the config manager/flow (or in rare cases by the provider itself) - value: ConfigValueType = None - - def parse_value( - self, - value: ConfigValueType, - allow_none: bool = True, - ) -> ConfigValueType: - """Parse value from the config entry details and plain value.""" - expected_type = list if self.multi_value else ConfigEntryTypeMap.get(self.type, NoneType) - if value is None: - value = self.default_value - if value is None and (not self.required or allow_none): - expected_type = NoneType - if self.type == ConfigEntryType.LABEL: - value = self.label - if not isinstance(value, expected_type): - # handle common conversions/mistakes - if expected_type is float and isinstance(value, int): - self.value = float(value) - return self.value - if expected_type is int and isinstance(value, float): - self.value = int(value) - return self.value - for val_type in (int, float): - # convert int/float from string - if expected_type == val_type and isinstance(value, str): - try: - self.value = val_type(value) - return self.value - except ValueError: - pass - if self.type in UI_ONLY: - self.value = self.default_value - return self.value - # fallback to default - if self.default_value is not None: - LOGGER.warning( - "%s has unexpected type: %s, fallback to default", - self.key, - type(self.value), - ) - value = self.default_value - if not (value is None and allow_none): - msg = f"{self.key} has unexpected type: {type(value)}" - raise ValueError(msg) - self.value = value - return self.value - - -@dataclass -class Config(DataClassDictMixin): - """Base Configuration object.""" - - values: dict[str, ConfigEntry] - - def get_value(self, key: str) -> ConfigValueType: - """Return config value for given key.""" - config_value = self.values[key] - if config_value.type == ConfigEntryType.SECURE_STRING and config_value.value: - assert isinstance(config_value.value, str) - assert DECRYPT_CALLBACK is not None - return DECRYPT_CALLBACK(config_value.value) - return config_value.value - - @classmethod - def parse( - cls, - config_entries: Iterable[ConfigEntry], - raw: dict[str, Any], - ) -> Config: - """Parse Config from the raw values (as stored in persistent storage).""" - conf = cls.from_dict({**raw, "values": {}}) - for entry in config_entries: - # unpack Enum value in default_value - if isinstance(entry.default_value, Enum): - entry.default_value = entry.default_value.value - # create a copy of the entry - conf.values[entry.key] = ConfigEntry.from_dict(entry.to_dict()) - conf.values[entry.key].parse_value( - raw.get("values", {}).get(entry.key), allow_none=True - ) - return conf - - def to_raw(self) -> dict[str, Any]: - """Return minimized/raw dict to store in persistent storage.""" - - def _handle_value(value: ConfigEntry) -> ConfigValueType: - if value.type == ConfigEntryType.SECURE_STRING: - assert isinstance(value.value, str) - assert ENCRYPT_CALLBACK is not None - return ENCRYPT_CALLBACK(value.value) - return value.value - - res = self.to_dict() - res["values"] = { - x.key: _handle_value(x) - for x in self.values.values() - if (x.value != x.default_value and x.type not in UI_ONLY) - } - return res - - def __post_serialize__(self, d: dict[str, Any]) -> dict[str, Any]: - """Adjust dict object after it has been serialized.""" - for key, value in self.values.items(): - # drop all password values from the serialized dict - # API consumers (including the frontend) are not allowed to retrieve it - # (even if its encrypted) but they can only set it. - if value.value and value.type == ConfigEntryType.SECURE_STRING: - d["values"][key]["value"] = SECURE_STRING_SUBSTITUTE - return d - - def update(self, update: dict[str, ConfigValueType]) -> set[str]: - """Update Config with updated values.""" - changed_keys: set[str] = set() - - # root values (enabled, name) - root_values = ("enabled", "name") - for key in root_values: - if key not in update: - continue - cur_val = getattr(self, key) - new_val = update[key] - if new_val == cur_val: - continue - setattr(self, key, new_val) - changed_keys.add(key) - - for key, new_val in update.items(): - if key in root_values: - continue - if key not in self.values: - continue - cur_val = self.values[key].value if key in self.values else None - # parse entry to do type validation - parsed_val = self.values[key].parse_value(new_val) - if cur_val != parsed_val: - changed_keys.add(f"values/{key}") - - return changed_keys - - def validate(self) -> None: - """Validate if configuration is valid.""" - # For now we just use the parse method to check for not allowed None values - # this can be extended later - for value in self.values.values(): - value.parse_value(value.value, allow_none=False) - - -@dataclass -class ProviderConfig(Config): - """Provider(instance) Configuration.""" - - type: ProviderType - domain: str - instance_id: str - # enabled: boolean to indicate if the provider is enabled - enabled: bool = True - # name: an (optional) custom name for this provider instance/config - name: str | None = None - # last_error: an optional error message if the provider could not be setup with this config - last_error: str | None = None - - -@dataclass -class PlayerConfig(Config): - """Player Configuration.""" - - provider: str - player_id: str - # enabled: boolean to indicate if the player is enabled - enabled: bool = True - # name: an (optional) custom name for this player - name: str | None = None - # available: boolean to indicate if the player is available - available: bool = True - # default_name: default name to use when there is no name available - default_name: str | None = None - - -@dataclass -class CoreConfig(Config): - """CoreController Configuration.""" - - domain: str # domain/name of the core module - # last_error: an optional error message if the module could not be setup with this config - last_error: str | None = None - - -CONF_ENTRY_LOG_LEVEL = ConfigEntry( - key=CONF_LOG_LEVEL, - type=ConfigEntryType.STRING, - label="Log level", - options=( - ConfigValueOption("global", "GLOBAL"), - ConfigValueOption("info", "INFO"), - ConfigValueOption("warning", "WARNING"), - ConfigValueOption("error", "ERROR"), - ConfigValueOption("debug", "DEBUG"), - ConfigValueOption("verbose", "VERBOSE"), - ), - default_value="GLOBAL", - category="advanced", -) - -DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) -DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) - -# some reusable player config entries - -CONF_ENTRY_FLOW_MODE = ConfigEntry( - key=CONF_FLOW_MODE, - type=ConfigEntryType.BOOLEAN, - label="Enable queue flow mode", - default_value=False, -) - -CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True} -) - -CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True} -) - -CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True} -) - - -CONF_ENTRY_AUTO_PLAY = ConfigEntry( - key=CONF_AUTO_PLAY, - type=ConfigEntryType.BOOLEAN, - label="Automatically play/resume on power on", - default_value=False, - description="When this player is turned ON, automatically start playing " - "(if there are items in the queue).", -) - -CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry( - key=CONF_OUTPUT_CHANNELS, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Stereo (both channels)", "stereo"), - ConfigValueOption("Left channel", "left"), - ConfigValueOption("Right channel", "right"), - ConfigValueOption("Mono (both channels)", "mono"), - ), - default_value="stereo", - label="Output Channel Mode", - category="audio", -) - -CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry( - key=CONF_VOLUME_NORMALIZATION, - type=ConfigEntryType.BOOLEAN, - label="Enable volume normalization", - default_value=True, - description="Enable volume normalization (EBU-R128 based)", - category="audio", -) - -CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_TARGET, - type=ConfigEntryType.INTEGER, - range=(-70, -5), - default_value=-17, - label="Target level for volume normalization", - description="Adjust average (perceived) loudness to this target level", - depends_on=CONF_VOLUME_NORMALIZATION, - category="advanced", -) - -CONF_ENTRY_EQ_BASS = ConfigEntry( - key=CONF_EQ_BASS, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: bass", - description="Use the builtin basic equalizer to adjust the bass of audio.", - category="audio", -) - -CONF_ENTRY_EQ_MID = ConfigEntry( - key=CONF_EQ_MID, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: midrange", - description="Use the builtin basic equalizer to adjust the midrange of audio.", - category="audio", -) - -CONF_ENTRY_EQ_TREBLE = ConfigEntry( - key=CONF_EQ_TREBLE, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: treble", - description="Use the builtin basic equalizer to adjust the treble of audio.", - category="audio", -) - - -CONF_ENTRY_CROSSFADE = ConfigEntry( - key=CONF_CROSSFADE, - type=ConfigEntryType.BOOLEAN, - label="Enable crossfade", - default_value=False, - description="Enable a crossfade transition between (queue) tracks.", - category="audio", -) - -CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED = ConfigEntry( - key=CONF_CROSSFADE, - type=ConfigEntryType.BOOLEAN, - label="Enable crossfade", - default_value=False, - description="Enable a crossfade transition between (queue) tracks.\n\n " - "Requires flow-mode to be enabled", - category="audio", - depends_on=CONF_FLOW_MODE, -) - -CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry( - key=CONF_CROSSFADE_DURATION, - type=ConfigEntryType.INTEGER, - range=(1, 10), - default_value=8, - label="Crossfade duration", - description="Duration in seconds of the crossfade between tracks (if enabled)", - depends_on=CONF_CROSSFADE, - category="advanced", -) - -CONF_ENTRY_HIDE_PLAYER = ConfigEntry( - key=CONF_HIDE_PLAYER, - type=ConfigEntryType.BOOLEAN, - label="Hide this player in the user interface", - default_value=False, -) - -CONF_ENTRY_ENFORCE_MP3 = ConfigEntry( - key=CONF_ENFORCE_MP3, - type=ConfigEntryType.BOOLEAN, - label="Enforce (lossy) mp3 stream", - default_value=False, - description="By default, Music Assistant sends lossless, high quality audio " - "to all players. Some players can not deal with that and require the stream to be packed " - "into a lossy mp3 codec. \n\n " - "Only enable when needed. Saves some bandwidth at the cost of audio quality.", - category="audio", -) - -CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True} -) - -CONF_ENTRY_SYNC_ADJUST = ConfigEntry( - key=CONF_SYNC_ADJUST, - type=ConfigEntryType.INTEGER, - range=(-500, 500), - default_value=0, - label="Audio synchronization delay correction", - description="If this player is playing audio synced with other players " - "and you always hear the audio too early or late on this player, " - "you can shift the audio a bit.", - category="advanced", -) - - -CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry( - key=CONF_TTS_PRE_ANNOUNCE, - type=ConfigEntryType.BOOLEAN, - default_value=True, - label="Pre-announce TTS announcements", - category="announcements", -) - - -CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_STRATEGY, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Absolute volume", "absolute"), - ConfigValueOption("Relative volume increase", "relative"), - ConfigValueOption("Volume increase by fixed percentage", "percentual"), - ConfigValueOption("Do not adjust volume", "none"), - ), - default_value="percentual", - label="Volume strategy for Announcements", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME, - type=ConfigEntryType.INTEGER, - default_value=85, - label="Volume for Announcements", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_MIN, - type=ConfigEntryType.INTEGER, - default_value=15, - label="Minimum Volume level for Announcements", - description="The volume (adjustment) of announcements should no go below this level.", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_MAX, - type=ConfigEntryType.INTEGER, - default_value=75, - label="Maximum Volume level for Announcements", - description="The volume (adjustment) of announcements should no go above this level.", - category="announcements", -) - -CONF_ENTRY_PLAYER_ICON = ConfigEntry( - key=CONF_ICON, - type=ConfigEntryType.ICON, - default_value="mdi-speaker", - label="Icon", - description="Material design icon for this player. " - "\n\nSee https://pictogrammers.com/library/mdi/", - category="generic", -) - -CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict( - {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"} -) - -CONF_ENTRY_SAMPLE_RATES = ConfigEntry( - key=CONF_SAMPLE_RATES, - type=ConfigEntryType.INTEGER_TUPLE, - options=( - ConfigValueOption("44.1kHz / 16 bits", (44100, 16)), - ConfigValueOption("44.1kHz / 24 bits", (44100, 24)), - ConfigValueOption("48kHz / 16 bits", (48000, 16)), - ConfigValueOption("48kHz / 24 bits", (48000, 24)), - ConfigValueOption("88.2kHz / 16 bits", (88200, 16)), - ConfigValueOption("88.2kHz / 24 bits", (88200, 24)), - ConfigValueOption("96kHz / 16 bits", (96000, 16)), - ConfigValueOption("96kHz / 24 bits", (96000, 24)), - ConfigValueOption("176.4kHz / 16 bits", (176400, 16)), - ConfigValueOption("176.4kHz / 24 bits", (176400, 24)), - ConfigValueOption("192kHz / 16 bits", (192000, 16)), - ConfigValueOption("192kHz / 24 bits", (192000, 24)), - ConfigValueOption("352.8kHz / 16 bits", (352800, 16)), - ConfigValueOption("352.8kHz / 24 bits", (352800, 24)), - ConfigValueOption("384kHz / 16 bits", (384000, 16)), - ConfigValueOption("384kHz / 24 bits", (384000, 24)), - ), - default_value=[(44100, 16), (48000, 16)], - required=True, - multi_value=True, - label="Sample rates supported by this player", - category="advanced", - description="The sample rates (and bit depths) supported by this player.\n" - "Content with unsupported sample rates will be automatically resampled.", -) - - -CONF_ENTRY_HTTP_PROFILE = ConfigEntry( - key=CONF_HTTP_PROFILE, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Profile 1 - chunked", "chunked"), - ConfigValueOption("Profile 2 - no content length", "no_content_length"), - ConfigValueOption("Profile 3 - forced content length", "forced_content_length"), - ), - default_value="no_content_length", - label="HTTP Profile used for sending audio", - category="advanced", - description="This is considered to be a very advanced setting, only adjust this if needed, " - "for example if your player stops playing halfway streams or if you experience " - "other playback related issues. In most cases the default setting is fine.", -) - -CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict( - {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked", "hidden": True} -) -CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict( - {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length", "hidden": True} -) - -CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry( - key=CONF_ENABLE_ICY_METADATA, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Disabled - do not send ICY metadata", "disabled"), - ConfigValueOption("Profile 1 - basic info", "basic"), - ConfigValueOption("Profile 2 - full info (including image)", "full"), - ), - depends_on=CONF_FLOW_MODE, - default_value="disabled", - label="Try to ingest metadata into stream (ICY)", - category="advanced", - description="Try to ingest metadata into the stream (ICY) to show track info on the player, " - "even when flow mode is enabled.\n\nThis is called ICY metadata and its what is also used by " - "online radio station to inform you what is playing. \n\nBe aware that not all players support " - "this correctly. If you experience issues with playback, try to disable this setting.", -) - - -def create_sample_rates_config_entry( - max_sample_rate: int, - max_bit_depth: int, - safe_max_sample_rate: int = 48000, - safe_max_bit_depth: int = 16, - hidden: bool = False, -) -> ConfigEntry: - """Create sample rates config entry based on player specific helpers.""" - assert CONF_ENTRY_SAMPLE_RATES.options - conf_entry = ConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict()) - conf_entry.hidden = hidden - options: list[ConfigValueOption] = [] - default_value: list[tuple[int, int]] = [] - for option in CONF_ENTRY_SAMPLE_RATES.options: - if not isinstance(option.value, tuple): - continue - sample_rate, bit_depth = option.value - if sample_rate <= max_sample_rate and bit_depth <= max_bit_depth: - options.append(option) - if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth: - default_value.append(option.value) - conf_entry.options = tuple(options) - conf_entry.default_value = default_value - return conf_entry - - -BASE_PLAYER_CONFIG_ENTRIES = ( - # config entries that are valid for all players - CONF_ENTRY_PLAYER_ICON, - CONF_ENTRY_FLOW_MODE, - CONF_ENTRY_VOLUME_NORMALIZATION, - CONF_ENTRY_AUTO_PLAY, - CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, - CONF_ENTRY_HIDE_PLAYER, - CONF_ENTRY_TTS_PRE_ANNOUNCE, - CONF_ENTRY_SAMPLE_RATES, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, -) diff --git a/music_assistant/common/models/enums.py b/music_assistant/common/models/enums.py deleted file mode 100644 index ecfef45b..00000000 --- a/music_assistant/common/models/enums.py +++ /dev/null @@ -1,460 +0,0 @@ -"""All enums used by the Music Assistant models.""" - -from __future__ import annotations - -import contextlib -from enum import EnumType, IntEnum, StrEnum - - -class MediaTypeMeta(EnumType): - """Class properties for MediaType.""" - - @property - def ALL(cls) -> list[MediaType]: # noqa: N802 - """All MediaTypes.""" - return [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - MediaType.RADIO, - ] - - -class MediaType(StrEnum, metaclass=MediaTypeMeta): - """Enum for MediaType.""" - - ARTIST = "artist" - ALBUM = "album" - TRACK = "track" - PLAYLIST = "playlist" - RADIO = "radio" - FOLDER = "folder" - ANNOUNCEMENT = "announcement" - FLOW_STREAM = "flow_stream" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> MediaType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ExternalID(StrEnum): - """Enum with External ID types.""" - - MB_ARTIST = "musicbrainz_artistid" # MusicBrainz Artist ID (or AlbumArtist ID) - MB_ALBUM = "musicbrainz_albumid" # MusicBrainz Album ID - MB_RELEASEGROUP = "musicbrainz_releasegroupid" # MusicBrainz ReleaseGroupID - MB_TRACK = "musicbrainz_trackid" # MusicBrainz Track ID - MB_RECORDING = "musicbrainz_recordingid" # MusicBrainz Recording ID - - ISRC = "isrc" # used to identify unique recordings - BARCODE = "barcode" # EAN-13 barcode for identifying albums - ACOUSTID = "acoustid" # unique fingerprint (id) for a recording - ASIN = "asin" # amazon unique number to identify albums - DISCOGS = "discogs" # id for media item on discogs - TADB = "tadb" # the audio db id - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ExternalID: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - @property - def is_unique(self) -> bool: - """Return if the ExternalID is unique.""" - return self.is_musicbrainz or self in ( - ExternalID.ACOUSTID, - ExternalID.DISCOGS, - ExternalID.TADB, - ) - - @property - def is_musicbrainz(self) -> bool: - """Return if the ExternalID is a MusicBrainz identifier.""" - return self in ( - ExternalID.MB_RELEASEGROUP, - ExternalID.MB_ALBUM, - ExternalID.MB_TRACK, - ExternalID.MB_ARTIST, - ExternalID.MB_RECORDING, - ) - - -class LinkType(StrEnum): - """Enum with link types.""" - - WEBSITE = "website" - FACEBOOK = "facebook" - TWITTER = "twitter" - LASTFM = "lastfm" - YOUTUBE = "youtube" - INSTAGRAM = "instagram" - SNAPCHAT = "snapchat" - TIKTOK = "tiktok" - DISCOGS = "discogs" - WIKIPEDIA = "wikipedia" - ALLMUSIC = "allmusic" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> LinkType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ImageType(StrEnum): - """Enum with image types.""" - - THUMB = "thumb" - LANDSCAPE = "landscape" - FANART = "fanart" - LOGO = "logo" - CLEARART = "clearart" - BANNER = "banner" - CUTOUT = "cutout" - BACK = "back" - DISCART = "discart" - OTHER = "other" - - @classmethod - def _missing_(cls, value: object) -> ImageType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.OTHER - - -class AlbumType(StrEnum): - """Enum for Album type.""" - - ALBUM = "album" - SINGLE = "single" - COMPILATION = "compilation" - EP = "ep" - PODCAST = "podcast" - AUDIOBOOK = "audiobook" - UNKNOWN = "unknown" - - -class ContentType(StrEnum): - """Enum with audio content/container types supported by ffmpeg.""" - - OGG = "ogg" - FLAC = "flac" - MP3 = "mp3" - AAC = "aac" - MPEG = "mpeg" - ALAC = "alac" - WAV = "wav" - AIFF = "aiff" - WMA = "wma" - M4A = "m4a" - MP4 = "mp4" - M4B = "m4b" - DSF = "dsf" - OPUS = "opus" - WAVPACK = "wv" - PCM_S16LE = "s16le" # PCM signed 16-bit little-endian - PCM_S24LE = "s24le" # PCM signed 24-bit little-endian - PCM_S32LE = "s32le" # PCM signed 32-bit little-endian - PCM_F32LE = "f32le" # PCM 32-bit floating-point little-endian - PCM_F64LE = "f64le" # PCM 64-bit floating-point little-endian - PCM = "pcm" # PCM generic (details determined later) - UNKNOWN = "?" - - @classmethod - def _missing_(cls, value: object) -> ContentType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - @classmethod - def try_parse(cls, string: str) -> ContentType: - """Try to parse ContentType from (url)string/extension.""" - tempstr = string.lower() - if "audio/" in tempstr: - tempstr = tempstr.split("/")[1] - for splitter in (".", ","): - if splitter in tempstr: - for val in tempstr.split(splitter): - with contextlib.suppress(ValueError): - parsed = cls(val.strip()) - if parsed != ContentType.UNKNOWN: - return parsed - tempstr = tempstr.split("?")[0] - tempstr = tempstr.split("&")[0] - tempstr = tempstr.split(";")[0] - tempstr = tempstr.replace("mp4", "m4a") - tempstr = tempstr.replace("mp4a", "m4a") - try: - return cls(tempstr) - except ValueError: - return cls.UNKNOWN - - def is_pcm(self) -> bool: - """Return if contentype is PCM.""" - return self.name.startswith("PCM") - - def is_lossless(self) -> bool: - """Return if format is lossless.""" - return self.is_pcm() or self in ( - ContentType.DSF, - ContentType.FLAC, - ContentType.AIFF, - ContentType.WAV, - ContentType.ALAC, - ContentType.WAVPACK, - ) - - @classmethod - def from_bit_depth(cls, bit_depth: int, floating_point: bool = False) -> ContentType: - """Return (PCM) Contenttype from PCM bit depth.""" - if floating_point and bit_depth > 32: - return cls.PCM_F64LE - if floating_point: - return cls.PCM_F32LE - if bit_depth == 16: - return cls.PCM_S16LE - if bit_depth == 24: - return cls.PCM_S24LE - return cls.PCM_S32LE - - -class QueueOption(StrEnum): - """Enum representation of the queue (play) options. - - - PLAY -> Insert new item(s) in queue at the current position and start playing. - - REPLACE -> Replace entire queue contents with the new items and start playing from index 0. - - NEXT -> Insert item(s) after current playing/buffered item. - - REPLACE_NEXT -> Replace item(s) after current playing/buffered item. - - ADD -> Add new item(s) to the queue (at the end if shuffle is not enabled). - """ - - PLAY = "play" - REPLACE = "replace" - NEXT = "next" - REPLACE_NEXT = "replace_next" - ADD = "add" - - -class RepeatMode(StrEnum): - """Enum with repeat modes.""" - - OFF = "off" # no repeat at all - ONE = "one" # repeat one/single track - ALL = "all" # repeat entire queue - - -class PlayerState(StrEnum): - """Enum for the (playback)state of a player.""" - - IDLE = "idle" - PAUSED = "paused" - PLAYING = "playing" - - -class PlayerType(StrEnum): - """Enum with possible Player Types. - - player: A regular player. - stereo_pair: Same as player but a dedicated stereo pair of 2 speakers. - group: A (dedicated) (sync)group player or (universal) playergroup. - """ - - PLAYER = "player" - STEREO_PAIR = "stereo_pair" - GROUP = "group" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> PlayerType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class PlayerFeature(StrEnum): - """Enum with possible Player features. - - power: The player has a dedicated power control. - volume: The player supports adjusting the volume. - mute: The player supports muting the volume. - sync: The player supports syncing with other players (of the same platform). - accurate_time: The player provides millisecond accurate timing information. - seek: The player supports seeking to a specific. - enqueue: The player supports (en)queuing of media items natively. - """ - - POWER = "power" - VOLUME_SET = "volume_set" - VOLUME_MUTE = "volume_mute" - PAUSE = "pause" - SYNC = "sync" - SEEK = "seek" - NEXT_PREVIOUS = "next_previous" - PLAY_ANNOUNCEMENT = "play_announcement" - ENQUEUE = "enqueue" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> PlayerFeature: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class EventType(StrEnum): - """Enum with possible Events.""" - - PLAYER_ADDED = "player_added" - PLAYER_UPDATED = "player_updated" - PLAYER_REMOVED = "player_removed" - PLAYER_SETTINGS_UPDATED = "player_settings_updated" - QUEUE_ADDED = "queue_added" - QUEUE_UPDATED = "queue_updated" - QUEUE_ITEMS_UPDATED = "queue_items_updated" - QUEUE_TIME_UPDATED = "queue_time_updated" - MEDIA_ITEM_PLAYED = "media_item_played" - SHUTDOWN = "application_shutdown" - MEDIA_ITEM_ADDED = "media_item_added" - MEDIA_ITEM_UPDATED = "media_item_updated" - MEDIA_ITEM_DELETED = "media_item_deleted" - PROVIDERS_UPDATED = "providers_updated" - PLAYER_CONFIG_UPDATED = "player_config_updated" - SYNC_TASKS_UPDATED = "sync_tasks_updated" - AUTH_SESSION = "auth_session" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> EventType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ProviderFeature(StrEnum): - """Enum with features for a Provider.""" - - # - # MUSICPROVIDER FEATURES - # - - # browse/explore/recommendations - BROWSE = "browse" - SEARCH = "search" - RECOMMENDATIONS = "recommendations" - - # library feature per mediatype - LIBRARY_ARTISTS = "library_artists" - LIBRARY_ALBUMS = "library_albums" - LIBRARY_TRACKS = "library_tracks" - LIBRARY_PLAYLISTS = "library_playlists" - LIBRARY_RADIOS = "library_radios" - - # additional library features - ARTIST_ALBUMS = "artist_albums" - ARTIST_TOPTRACKS = "artist_toptracks" - - # library edit (=add/remove) feature per mediatype - LIBRARY_ARTISTS_EDIT = "library_artists_edit" - LIBRARY_ALBUMS_EDIT = "library_albums_edit" - LIBRARY_TRACKS_EDIT = "library_tracks_edit" - LIBRARY_PLAYLISTS_EDIT = "library_playlists_edit" - LIBRARY_RADIOS_EDIT = "library_radios_edit" - - # if we can grab 'similar tracks' from the music provider - # used to generate dynamic playlists - SIMILAR_TRACKS = "similar_tracks" - - # playlist-specific features - PLAYLIST_TRACKS_EDIT = "playlist_tracks_edit" - PLAYLIST_CREATE = "playlist_create" - - # - # PLAYERPROVIDER FEATURES - # - SYNC_PLAYERS = "sync_players" - REMOVE_PLAYER = "remove_player" - - # - # METADATAPROVIDER FEATURES - # - ARTIST_METADATA = "artist_metadata" - ALBUM_METADATA = "album_metadata" - TRACK_METADATA = "track_metadata" - - # - # PLUGIN FEATURES - # - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ProviderFeature: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ProviderType(StrEnum): - """Enum with supported provider types.""" - - MUSIC = "music" - PLAYER = "player" - METADATA = "metadata" - PLUGIN = "plugin" - CORE = "core" - - -class ConfigEntryType(StrEnum): - """Enum for the type of a config entry.""" - - BOOLEAN = "boolean" - STRING = "string" - SECURE_STRING = "secure_string" - INTEGER = "integer" - FLOAT = "float" - LABEL = "label" - INTEGER_TUPLE = "integer_tuple" - DIVIDER = "divider" - ACTION = "action" - ICON = "icon" - ALERT = "alert" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ConfigEntryType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class StreamType(StrEnum): - """Enum for the type of streamdetails.""" - - HTTP = "http" # regular http stream - ENCRYPTED_HTTP = "encrypted_http" # encrypted http stream - HLS = "hls" # http HLS stream - ICY = "icy" # http stream with icy metadata - LOCAL_FILE = "local_file" - CUSTOM = "custom" - - -class CacheCategory(IntEnum): - """Enum with predefined cache categories.""" - - DEFAULT = 0 - MUSIC_SEARCH = 1 - MUSIC_ALBUM_TRACKS = 2 - MUSIC_ARTIST_TRACKS = 3 - MUSIC_ARTIST_ALBUMS = 4 - MUSIC_PLAYLIST_TRACKS = 5 - MUSIC_PROVIDER_ITEM = 6 - PLAYER_QUEUE_STATE = 7 - MEDIA_INFO = 8 - LIBRARY_ITEMS = 9 - - -class VolumeNormalizationMode(StrEnum): - """Enum with possible VolumeNormalization modes.""" - - DISABLED = "disabled" - DYNAMIC = "dynamic" - MEASUREMENT_ONLY = "measurement_only" - FALLBACK_FIXED_GAIN = "fallback_fixed_gain" - FIXED_GAIN = "fixed_gain" - FALLBACK_DYNAMIC = "fallback_dynamic" diff --git a/music_assistant/common/models/errors.py b/music_assistant/common/models/errors.py deleted file mode 100644 index 5e04af31..00000000 --- a/music_assistant/common/models/errors.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Custom errors and exceptions.""" - - -class MusicAssistantError(Exception): - """Custom Exception for all errors.""" - - error_code = 0 - - def __init_subclass__(cls, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - """Register a subclass.""" - super().__init_subclass__(*args, **kwargs) - ERROR_MAP[cls.error_code] = cls - - -# mapping from error_code to Exception class -ERROR_MAP: dict[int, type] = {0: MusicAssistantError, 999: MusicAssistantError} - - -class ProviderUnavailableError(MusicAssistantError): - """Error raised when trying to access mediaitem of unavailable provider.""" - - error_code = 1 - - -class MediaNotFoundError(MusicAssistantError): - """Error raised when trying to access non existing media item.""" - - error_code = 2 - - -class InvalidDataError(MusicAssistantError): - """Error raised when an object has invalid data.""" - - error_code = 3 - - -class AlreadyRegisteredError(MusicAssistantError): - """Error raised when a duplicate music provider or player is registered.""" - - error_code = 4 - - -class SetupFailedError(MusicAssistantError): - """Error raised when setup of a provider or player failed.""" - - error_code = 5 - - -class LoginFailed(MusicAssistantError): - """Error raised when a login failed.""" - - error_code = 6 - - -class AudioError(MusicAssistantError): - """Error raised when an issue arrised when processing audio.""" - - error_code = 7 - - -class QueueEmpty(MusicAssistantError): - """Error raised when trying to start queue stream while queue is empty.""" - - error_code = 8 - - -class UnsupportedFeaturedException(MusicAssistantError): - """Error raised when a feature is not supported.""" - - error_code = 9 - - -class PlayerUnavailableError(MusicAssistantError): - """Error raised when trying to access non-existing or unavailable player.""" - - error_code = 10 - - -class PlayerCommandFailed(MusicAssistantError): - """Error raised when a command to a player failed execution.""" - - error_code = 11 - - -class InvalidCommand(MusicAssistantError): - """Error raised when an unknown command is requested on the API.""" - - error_code = 12 - - -class UnplayableMediaError(MusicAssistantError): - """Error thrown when a MediaItem cannot be played properly.""" - - error_code = 13 - - -class InvalidProviderURI(MusicAssistantError): - """Error thrown when a provider URI does not match a known format.""" - - error_code = 14 - - -class InvalidProviderID(MusicAssistantError): - """Error thrown when a provider media item identifier does not match a known format.""" - - error_code = 15 - - -class RetriesExhausted(MusicAssistantError): - """Error thrown when a retries to a given provider URI have been exhausted.""" - - error_code = 16 - - -class ResourceTemporarilyUnavailable(MusicAssistantError): - """Error thrown when a resource is temporarily unavailable.""" - - def __init__(self, *args: object, backoff_time: int = 0) -> None: - """Initialize.""" - super().__init__(*args) - self.backoff_time = backoff_time - - error_code = 17 - - -class ProviderPermissionDenied(MusicAssistantError): - """Error thrown when a provider action is denied because of permissions.""" - - error_code = 18 - - -class ActionUnavailable(MusicAssistantError): - """Error thrown when a action is denied because is is (temporary) unavailable/not possible.""" - - error_code = 19 diff --git a/music_assistant/common/models/event.py b/music_assistant/common/models/event.py deleted file mode 100644 index a997808f..00000000 --- a/music_assistant/common/models/event.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Model for Music Assistant Event.""" - -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import get_serializable_value -from music_assistant.common.models.enums import EventType - - -@dataclass -class MassEvent(DataClassORJSONMixin): - """Representation of an Event emitted in/by Music Assistant.""" - - event: EventType - object_id: str | None = None # player_id, queue_id or uri - data: Any = field( - default=None, - metadata={"serialize": lambda v: get_serializable_value(v)}, - ) diff --git a/music_assistant/common/models/media_items.py b/music_assistant/common/models/media_items.py deleted file mode 100644 index 024f8d07..00000000 --- a/music_assistant/common/models/media_items.py +++ /dev/null @@ -1,585 +0,0 @@ -"""Models and helpers for media items.""" - -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from dataclasses import dataclass, field, fields -from typing import TYPE_CHECKING, Any, TypeGuard, TypeVar, cast - -from mashumaro import DataClassDictMixin - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.uri import create_uri -from music_assistant.common.helpers.util import create_sort_name, is_valid_uuid, merge_lists -from music_assistant.common.models.enums import ( - AlbumType, - ContentType, - ExternalID, - ImageType, - LinkType, - MediaType, -) -from music_assistant.common.models.errors import InvalidDataError - -MetadataTypes = int | bool | str | list[str] - -_T = TypeVar("_T") - - -class UniqueList(list[_T]): - """Custom list that ensures the inserted items are unique.""" - - def __init__(self, iterable: Iterable[_T] | None = None) -> None: - """Initialize.""" - if not iterable: - super().__init__() - return - seen: set[_T] = set() - seen_add = seen.add - super().__init__(x for x in iterable if not (x in seen or seen_add(x))) - - def append(self, item: _T) -> None: - """Append item.""" - if item in self: - return - super().append(item) - - def extend(self, other: Iterable[_T]) -> None: - """Extend list.""" - other = [x for x in other if x not in self] - super().extend(other) - - -@dataclass(kw_only=True) -class AudioFormat(DataClassDictMixin): - """Model for AudioFormat details.""" - - content_type: ContentType = ContentType.UNKNOWN - sample_rate: int = 44100 - bit_depth: int = 16 - channels: int = 2 - output_format_str: str = "" - bit_rate: int | None = None # optional bitrate in kbps - - def __post_init__(self) -> None: - """Execute actions after init.""" - if not self.output_format_str and self.content_type.is_pcm(): - self.output_format_str = ( - f"pcm;codec=pcm;rate={self.sample_rate};" - f"bitrate={self.bit_depth};channels={self.channels}" - ) - elif not self.output_format_str: - self.output_format_str = self.content_type.value - if self.bit_rate and self.bit_rate > 100000: - # correct bit rate in bits per second to kbps - self.bit_rate = int(self.bit_rate / 1000) - - @property - def quality(self) -> int: - """Calculate quality score.""" - if self.content_type.is_lossless(): - # lossless content is scored very high based on sample rate and bit depth - return int(self.sample_rate / 1000) + self.bit_depth - # lossy content, bit_rate is most important score - # but prefer some codecs over others - # calculate a rough score based on bit rate per channel - bit_rate = self.bit_rate or 320 - bit_rate_score = (bit_rate / self.channels) / 100 - if self.content_type in (ContentType.AAC, ContentType.OGG): - bit_rate_score += 1 - return int(bit_rate_score) - - @property - def pcm_sample_size(self) -> int: - """Return the PCM sample size.""" - return int(self.sample_rate * (self.bit_depth / 8) * self.channels) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, AudioFormat): - return False - return self.output_format_str == other.output_format_str - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # bit_rate is now optional. Set default value to keep compatibility - # TODO: remove this after release of MA 2.5 - d["bit_rate"] = d["bit_rate"] or 0 - return d - - -@dataclass(kw_only=True) -class ProviderMapping(DataClassDictMixin): - """Model for a MediaItem's provider mapping details.""" - - item_id: str - provider_domain: str - provider_instance: str - available: bool = True - # quality/audio details (streamable content only) - audio_format: AudioFormat = field(default_factory=AudioFormat) - # url = link to provider details page if exists - url: str | None = None - # optional details to store provider specific details - details: str | None = None - - @property - def quality(self) -> int: - """Return quality score.""" - quality = self.audio_format.quality - # append provider score so filebased providers are scored higher - return quality + self.priority - - @property - def priority(self) -> int: - """Return priority score to sort local providers before online.""" - if not (local_provs := get_global_cache_value("non_streaming_providers")): - # this is probably the client - return 0 - if TYPE_CHECKING: - local_provs = cast(set[str], local_provs) - if self.provider_domain in ("filesystem_local", "filesystem_smb"): - return 2 - if self.provider_instance in local_provs: - return 1 - return 0 - - def __hash__(self) -> int: - """Return custom hash.""" - return hash((self.provider_instance, self.item_id)) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, ProviderMapping): - return False - return self.provider_instance == other.provider_instance and self.item_id == other.item_id - - -@dataclass(frozen=True, kw_only=True) -class MediaItemLink(DataClassDictMixin): - """Model for a link.""" - - type: LinkType - url: str - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.type) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemLink): - return False - return self.url == other.url - - -@dataclass(frozen=True, kw_only=True) -class MediaItemImage(DataClassDictMixin): - """Model for a image.""" - - type: ImageType - path: str - provider: str # provider lookup key (only use instance id for fileproviders) - remotely_accessible: bool = False # url that is accessible from anywhere - - def __hash__(self) -> int: - """Return custom hash.""" - return hash((self.type.value, self.provider, self.path)) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemImage): - return False - return self.__hash__() == other.__hash__() - - -@dataclass(frozen=True, kw_only=True) -class MediaItemChapter(DataClassDictMixin): - """Model for a chapter.""" - - chapter_id: int - position_start: float - position_end: float | None = None - title: str | None = None - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.chapter_id) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemChapter): - return False - return self.chapter_id == other.chapter_id - - -@dataclass(kw_only=True) -class MediaItemMetadata(DataClassDictMixin): - """Model for a MediaItem's metadata.""" - - description: str | None = None - review: str | None = None - explicit: bool | None = None - # NOTE: images is a list of available images, sorted by preference - images: UniqueList[MediaItemImage] | None = None - genres: set[str] | None = None - mood: str | None = None - style: str | None = None - copyright: str | None = None - lyrics: str | None = None # tracks only - label: str | None = None - links: set[MediaItemLink] | None = None - chapters: UniqueList[MediaItemChapter] | None = None - performers: set[str] | None = None - preview: str | None = None - popularity: int | None = None - # last_refresh: timestamp the (full) metadata was last collected - last_refresh: int | None = None - - def update( - self, - new_values: MediaItemMetadata, - ) -> MediaItemMetadata: - """Update metadata (in-place) with new values.""" - if not new_values: - return self - for fld in fields(self): - new_val = getattr(new_values, fld.name) - if new_val is None: - continue - cur_val = getattr(self, fld.name) - if isinstance(cur_val, list) and isinstance(new_val, list): - new_val = UniqueList(merge_lists(cur_val, new_val)) - setattr(self, fld.name, new_val) - elif isinstance(cur_val, set) and isinstance(new_val, set | list | tuple): - cur_val.update(new_val) - elif new_val and fld.name in ( - "popularity", - "last_refresh", - "cache_checksum", - ): - # some fields are always allowed to be overwritten - # (such as checksum and last_refresh) - setattr(self, fld.name, new_val) - elif cur_val is None: - setattr(self, fld.name, new_val) - return self - - -@dataclass(kw_only=True) -class _MediaItemBase(DataClassDictMixin): - """Base representation of a Media Item or ItemMapping item object.""" - - item_id: str - provider: str # provider instance id or provider domain - name: str - version: str = "" - # sort_name will be auto generated if omitted - sort_name: str | None = None - # uri is auto generated, do not override unless really needed - uri: str | None = None - external_ids: set[tuple[ExternalID, str]] = field(default_factory=set) - media_type: MediaType = MediaType.UNKNOWN - - def __post_init__(self) -> None: - """Call after init.""" - if self.uri is None: - self.uri = create_uri(self.media_type, self.provider, self.item_id) - if self.sort_name is None: - self.sort_name = create_sort_name(self.name) - - def get_external_id(self, external_id_type: ExternalID) -> str | None: - """Get (the first instance) of given External ID or None if not found.""" - for ext_id in self.external_ids: - if ext_id[0] != external_id_type: - continue - return ext_id[1] - return None - - def add_external_id(self, external_id_type: ExternalID, value: str) -> None: - """Add ExternalID.""" - if external_id_type.is_musicbrainz and not is_valid_uuid(value): - msg = f"Invalid MusicBrainz identifier: {value}" - raise InvalidDataError(msg) - if external_id_type.is_unique and ( - existing := next((x for x in self.external_ids if x[0] == external_id_type), None) - ): - self.external_ids.remove(existing) - self.external_ids.add((external_id_type, value)) - - @property - def mbid(self) -> str | None: - """Return MusicBrainz ID.""" - if self.media_type == MediaType.ARTIST: - return self.get_external_id(ExternalID.MB_ARTIST) - if self.media_type == MediaType.ALBUM: - return self.get_external_id(ExternalID.MB_ALBUM) - if self.media_type == MediaType.TRACK: - return self.get_external_id(ExternalID.MB_RECORDING) - return None - - @mbid.setter - def mbid(self, value: str) -> None: - """Set MusicBrainz External ID.""" - if self.media_type == MediaType.ARTIST: - self.add_external_id(ExternalID.MB_ARTIST, value) - elif self.media_type == MediaType.ALBUM: - self.add_external_id(ExternalID.MB_ALBUM, value) - elif self.media_type == MediaType.TRACK: - # NOTE: for tracks we use the recording id to - # differentiate a unique recording - # and not the track id (as that is just the reference - # of the recording on a specific album) - self.add_external_id(ExternalID.MB_RECORDING, value) - return - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.uri) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItem | ItemMapping): - return False - return self.uri == other.uri - - -@dataclass(kw_only=True) -class MediaItem(_MediaItemBase): - """Base representation of a media item.""" - - __eq__ = _MediaItemBase.__eq__ - - provider_mappings: set[ProviderMapping] - # optional fields below - metadata: MediaItemMetadata = field(default_factory=MediaItemMetadata) - favorite: bool = False - position: int | None = None # required for playlist tracks, optional for all other - - def __hash__(self) -> int: - """Return hash of MediaItem.""" - return super().__hash__() - - @property - def available(self) -> bool: - """Return (calculated) availability.""" - if not (available_providers := get_global_cache_value("unique_providers")): - # this is probably the client - return any(x.available for x in self.provider_mappings) - if TYPE_CHECKING: - available_providers = cast(set[str], available_providers) - for x in self.provider_mappings: - if available_providers.intersection({x.provider_domain, x.provider_instance}): - return True - return False - - @property - def image(self) -> MediaItemImage | None: - """Return (first/random) image/thumb from metadata (if any).""" - if self.metadata is None or self.metadata.images is None: - return None - return next((x for x in self.metadata.images if x.type == ImageType.THUMB), None) - - -@dataclass(kw_only=True) -class ItemMapping(_MediaItemBase): - """Representation of a minimized item object.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - available: bool = True - image: MediaItemImage | None = None - - @classmethod - def from_item(cls, item: MediaItem | ItemMapping) -> ItemMapping: - """Create ItemMapping object from regular item.""" - if isinstance(item, ItemMapping): - return item - thumb_image = None - if item.metadata and item.metadata.images: - for img in item.metadata.images: - if img.type != ImageType.THUMB: - continue - thumb_image = img - break - return cls.from_dict( - {**item.to_dict(), "image": thumb_image.to_dict() if thumb_image else None} - ) - - -@dataclass(kw_only=True) -class Artist(MediaItem): - """Model for an artist.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.ARTIST - - -@dataclass(kw_only=True) -class Album(MediaItem): - """Model for an album.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.ALBUM - version: str = "" - year: int | None = None - artists: UniqueList[Artist | ItemMapping] = field(default_factory=UniqueList) - album_type: AlbumType = AlbumType.UNKNOWN - - @property - def artist_str(self) -> str: - """Return (combined) artist string for track.""" - return "/".join(x.name for x in self.artists) - - -@dataclass(kw_only=True) -class Track(MediaItem): - """Model for a track.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.TRACK - duration: int = 0 - version: str = "" - artists: UniqueList[Artist | ItemMapping] = field(default_factory=UniqueList) - album: Album | ItemMapping | None = None # required for album tracks - disc_number: int = 0 # required for album tracks - track_number: int = 0 # required for album tracks - - @property - def has_chapters(self) -> bool: - """ - Return boolean if this Track has chapters. - - This is often an indicator that this track is an episode from a - Podcast or AudioBook. - """ - if not self.metadata: - return False - if not self.metadata.chapters: - return False - return len(self.metadata.chapters) > 1 - - @property - def image(self) -> MediaItemImage | None: - """Return (first) image from metadata (prefer album).""" - if isinstance(self.album, Album) and self.album.image: - return self.album.image - return super().image - - @property - def artist_str(self) -> str: - """Return (combined) artist string for track.""" - return "/".join(x.name for x in self.artists) - - -@dataclass(kw_only=True) -class PlaylistTrack(Track): - """ - Model for a track on a playlist. - - Same as regular Track but with explicit and required definition of position. - """ - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - position: int - - -@dataclass(kw_only=True) -class Playlist(MediaItem): - """Model for a playlist.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.PLAYLIST - owner: str = "" - is_editable: bool = False - - # cache_checksum: optional value to (in)validate cache - # detect changes to the playlist tracks listing - cache_checksum: str | None = None - - -@dataclass(kw_only=True) -class Radio(MediaItem): - """Model for a radio station.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.RADIO - duration: int = 172800 - - -@dataclass(kw_only=True) -class BrowseFolder(MediaItem): - """Representation of a Folder used in Browse (which contains media items).""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.FOLDER - # path: the path (in uri style) to/for this browse folder - path: str = "" - # label: a labelid that needs to be translated by the frontend - label: str = "" - provider_mappings: set[ProviderMapping] = field(default_factory=set) - - def __post_init__(self) -> None: - """Call after init.""" - super().__post_init__() - if not self.path: - self.path = f"{self.provider}://{self.item_id}" - if not self.provider_mappings: - self.provider_mappings.add( - ProviderMapping( - item_id=self.item_id, - provider_domain=self.provider, - provider_instance=self.provider, - ) - ) - - -MediaItemType = Artist | Album | PlaylistTrack | Track | Radio | Playlist | BrowseFolder - - -@dataclass(kw_only=True) -class SearchResults(DataClassDictMixin): - """Model for results from a search query.""" - - artists: Sequence[Artist | ItemMapping] = field(default_factory=list) - albums: Sequence[Album | ItemMapping] = field(default_factory=list) - tracks: Sequence[Track | ItemMapping] = field(default_factory=list) - playlists: Sequence[Playlist | ItemMapping] = field(default_factory=list) - radio: Sequence[Radio | ItemMapping] = field(default_factory=list) - - -def media_from_dict(media_item: dict[str, Any]) -> MediaItemType | ItemMapping: - """Return MediaItem from dict.""" - if "provider_mappings" not in media_item: - return ItemMapping.from_dict(media_item) - if media_item["media_type"] == "artist": - return Artist.from_dict(media_item) - if media_item["media_type"] == "album": - return Album.from_dict(media_item) - if media_item["media_type"] == "track": - return Track.from_dict(media_item) - if media_item["media_type"] == "playlist": - return Playlist.from_dict(media_item) - if media_item["media_type"] == "radio": - return Radio.from_dict(media_item) - raise InvalidDataError("Unknown media type") - - -def is_track(val: MediaItem) -> TypeGuard[Track]: - """Return true if this MediaItem is a track.""" - return val.media_type == MediaType.TRACK diff --git a/music_assistant/common/models/player.py b/music_assistant/common/models/player.py deleted file mode 100644 index 35fb66ad..00000000 --- a/music_assistant/common/models/player.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Model(s) for Player.""" - -from __future__ import annotations - -import time -from dataclasses import dataclass, field -from typing import Any - -from mashumaro import DataClassDictMixin - -from .enums import MediaType, PlayerFeature, PlayerState, PlayerType - - -@dataclass(frozen=True) -class DeviceInfo(DataClassDictMixin): - """Model for a player's deviceinfo.""" - - model: str = "Unknown model" - address: str = "" - manufacturer: str = "Unknown Manufacturer" - - -@dataclass -class PlayerMedia(DataClassDictMixin): - """Metadata of Media loading/loaded into a player.""" - - uri: str # uri or other identifier of the loaded media - media_type: MediaType = MediaType.UNKNOWN - title: str | None = None # optional - artist: str | None = None # optional - album: str | None = None # optional - image_url: str | None = None # optional - duration: int | None = None # optional - queue_id: str | None = None # only present for requests from queue controller - queue_item_id: str | None = None # only present for requests from queue controller - custom_data: dict[str, Any] | None = None # optional - - -@dataclass -class Player(DataClassDictMixin): - """Representation of a Player within Music Assistant.""" - - player_id: str - provider: str # instance_id of the player provider - type: PlayerType - name: str - available: bool - powered: bool - device_info: DeviceInfo - supported_features: tuple[PlayerFeature, ...] = field(default=()) - - elapsed_time: float | None = None - elapsed_time_last_updated: float | None = None - state: PlayerState | None = None - volume_level: int | None = None - volume_muted: bool | None = None - - # group_childs: Return list of player group child id's or synced child`s. - # - If this player is a dedicated group player, - # returns all child id's of the players in the group. - # - If this is a syncgroup of players from the same platform (e.g. sonos), - # this will return the id's of players synced to this player, - # and this may include the player's own id. - group_childs: set[str] = field(default_factory=set) - - # active_source: return active source for this player - # can be set to a MA queue id or some player specific source - active_source: str | None = None - - # active_source: return player_id of the active group for this player (if any) - # if the player is grouped and a group is active, - # this should be set to the group's player_id by the group player implementation. - active_group: str | None = None - - # current_media: return current active/loaded item on the player - # this may be a MA queue item, url, uri or some provider specific string - # includes metadata if supported by the provider/player - current_media: PlayerMedia | None = None - - # synced_to: player_id of the player this player is currently synced to - # also referred to as "sync master" - synced_to: str | None = None - - # enabled_by_default: if the player is enabled by default - # can be used by a player provider to exclude some sort of players - enabled_by_default: bool = True - - # needs_poll: bool that can be set by the player(provider) - # if this player needs to be polled for state changes by the player manager - needs_poll: bool = False - - # poll_interval: a (dynamic) interval in seconds to poll the player (used with needs_poll) - poll_interval: int = 30 - - # - # THE BELOW ATTRIBUTES ARE MANAGED BY CONFIG AND THE PLAYER MANAGER - # - - # enabled: if the player is enabled - # will be set by the player manager based on config - # a disabled player is hidden in the UI and updates will not be processed - # nor will it be added to the HA integration - enabled: bool = True - - # hidden: if the player is hidden in the UI - # will be set by the player manager based on config - # a hidden player is hidden in the UI only but can still be controlled - hidden: bool = False - - # icon: material design icon for this player - # will be set by the player manager based on config - icon: str = "mdi-speaker" - - # group_volume: if the player is a player group or syncgroup master, - # this will return the average volume of all child players - # if not a group player, this is just the player's volume - group_volume: int = 100 - - # display_name: return final/corrected name of the player - # always prefers any overridden name from settings - display_name: str = "" - - # extra_data: any additional data to store on the player object - # and pass along freely - extra_data: dict[str, Any] = field(default_factory=dict) - - # announcement_in_progress boolean to indicate there's an announcement in progress. - announcement_in_progress: bool = False - - # last_poll: when was the player last polled (used with needs_poll) - last_poll: float = 0 - - # internal use only - _prev_volume_level: int = 0 - - @property - def corrected_elapsed_time(self) -> float | None: - """Return the corrected/realtime elapsed time.""" - if self.elapsed_time is None or self.elapsed_time_last_updated is None: - return None - if self.state == PlayerState.PLAYING: - return self.elapsed_time + (time.time() - self.elapsed_time_last_updated) - return self.elapsed_time - - @property - def current_item_id(self) -> str | None: - """Return current_item_id from current_media (if exists).""" - if self.current_media: - return self.current_media.uri - return None - - @current_item_id.setter - def current_item_id(self, uri: str) -> None: - """Set current_item_id (for backwards compatibility).""" - self.current_media = PlayerMedia(uri) - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # TEMP: convert values to prevent api breakage - # this may be removed after 2.3 has been launched to stable - if self.elapsed_time is None: - d["elapsed_time"] = 0 - if self.elapsed_time_last_updated is None: - d["elapsed_time_last_updated"] = 0 - if self.volume_level is None: - d["volume_level"] = 0 - if self.volume_muted is None: - d["volume_muted"] = False - if self.state is None: - d["state"] = PlayerState.IDLE - return d diff --git a/music_assistant/common/models/player_queue.py b/music_assistant/common/models/player_queue.py deleted file mode 100644 index dbeca325..00000000 --- a/music_assistant/common/models/player_queue.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Model(s) for PlayerQueue.""" - -from __future__ import annotations - -import time -from dataclasses import dataclass, field -from typing import Any, Self - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.media_items import MediaItemType -from music_assistant.constants import FALLBACK_DURATION - -from .enums import PlayerState, RepeatMode -from .queue_item import QueueItem - - -@dataclass -class PlayLogEntry: - """Representation of a PlayLogEntry within Music Assistant.""" - - queue_item_id: str - duration: int = FALLBACK_DURATION - seconds_streamed: float | None = None - - -@dataclass -class PlayerQueue(DataClassDictMixin): - """Representation of a PlayerQueue within Music Assistant.""" - - queue_id: str - active: bool - display_name: str - available: bool - items: int - - shuffle_enabled: bool = False - repeat_mode: RepeatMode = RepeatMode.OFF - dont_stop_the_music_enabled: bool = False - # current_index: index that is active (e.g. being played) by the player - current_index: int | None = None - # index_in_buffer: index that has been preloaded/buffered by the player - index_in_buffer: int | None = None - elapsed_time: float = 0 - elapsed_time_last_updated: float = time.time() - state: PlayerState = PlayerState.IDLE - current_item: QueueItem | None = None - next_item: QueueItem | None = None - radio_source: list[MediaItemType] = field(default_factory=list) - enqueued_media_items: list[MediaItemType] = field(default_factory=list) - flow_mode: bool = False - resume_pos: int = 0 - flow_mode_stream_log: list[PlayLogEntry] = field(default_factory=list) - next_track_enqueued: str | None = None - - @property - def corrected_elapsed_time(self) -> float: - """Return the corrected/realtime elapsed time.""" - return self.elapsed_time + (time.time() - self.elapsed_time_last_updated) - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - d.pop("flow_mode_stream_log", None) - d.pop("enqueued_media_items", None) - d.pop("next_track_enqueued", None) - return d - - def to_cache(self) -> dict[str, Any]: - """Return the dict that is suitable for storing into the cache db.""" - d = self.to_dict() - d.pop("current_item", None) - d.pop("next_item", None) - d.pop("index_in_buffer", None) - d.pop("flow_mode", None) - return d - - @classmethod - def from_cache(cls, d: dict[Any, Any]) -> Self: - """Restore a PlayerQueue from a cache dict.""" - d.pop("current_item", None) - d.pop("next_item", None) - d.pop("index_in_buffer", None) - d.pop("flow_mode", None) - d.pop("enqueued_media_items", None) - d.pop("next_track_enqueued", None) - d.pop("flow_mode_stream_log", None) - return cls.from_dict(d) diff --git a/music_assistant/common/models/provider.py b/music_assistant/common/models/provider.py deleted file mode 100644 index 7a7d0219..00000000 --- a/music_assistant/common/models/provider.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Models for providers and plugins in the MA ecosystem.""" - -from __future__ import annotations - -import asyncio -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import load_json_file - -from .enums import MediaType, ProviderFeature, ProviderType - - -@dataclass -class ProviderManifest(DataClassORJSONMixin): - """ProviderManifest, details of a provider.""" - - type: ProviderType - domain: str - name: str - description: str - codeowners: list[str] - - # optional params - - # requirements: list of (pip style) python packages required for this provider - requirements: list[str] = field(default_factory=list) - # documentation: link/url to documentation. - documentation: str | None = None - # multi_instance: whether multiple instances of the same provider are allowed/possible - multi_instance: bool = False - # builtin: whether this provider is a system/builtin provider, loaded by default - builtin: bool = False - # allow_disable: whether this provider can be disabled (used with builtin) - allow_disable: bool = True - # depends_on: depends on another provider to function - depends_on: str | None = None - # icon: name of the material design icon (https://pictogrammers.com/library/mdi) - icon: str | None = None - # icon_svg: svg icon (full xml string) - # if this attribute is omitted and an icon.svg is found in the provider - # folder, the file contents will be read instead. - icon_svg: str | None = None - # icon_svg_dark: optional separate dark svg icon (full xml string) - # if this attribute is omitted and an icon_dark.svg is found in the provider - # folder, the file contents will be read instead. - icon_svg_dark: str | None = None - # mdns_discovery: list of mdns types to discover - mdns_discovery: list[str] | None = None - - @classmethod - async def parse(cls, manifest_file: str) -> ProviderManifest: - """Parse ProviderManifest from file.""" - return await load_json_file(manifest_file, ProviderManifest) - - -@dataclass -class ProviderInstance(DataClassORJSONMixin): - """Provider instance details when a provider is serialized over the api.""" - - type: ProviderType - domain: str - name: str - instance_id: str - lookup_key: str - supported_features: list[ProviderFeature] - available: bool - icon: str | None = None - is_streaming_provider: bool | None = None # music providers only - - -@dataclass -class SyncTask: - """Description of a Sync task/job of a musicprovider.""" - - provider_domain: str - provider_instance: str - media_types: tuple[MediaType, ...] - task: asyncio.Task[None] | None - - def to_dict(self) -> dict[str, Any]: - """Return SyncTask as (serializable) dict.""" - # ruff: noqa:ARG002 - return { - "provider_domain": self.provider_domain, - "provider_instance": self.provider_instance, - "media_types": [x.value for x in self.media_types], - } diff --git a/music_assistant/common/models/queue_item.py b/music_assistant/common/models/queue_item.py deleted file mode 100644 index 8290b387..00000000 --- a/music_assistant/common/models/queue_item.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Model a QueueItem.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Self -from uuid import uuid4 - -from mashumaro import DataClassDictMixin - -from .enums import MediaType -from .media_items import ItemMapping, MediaItemImage, Radio, Track, UniqueList, is_track -from .streamdetails import StreamDetails - - -@dataclass -class QueueItem(DataClassDictMixin): - """Representation of a queue item.""" - - queue_id: str - queue_item_id: str - name: str - duration: int | None - sort_index: int = 0 - streamdetails: StreamDetails | None = None - media_item: Track | Radio | None = None - image: MediaItemImage | None = None - index: int = 0 - - def __post_init__(self) -> None: - """Set default values.""" - if not self.name and self.streamdetails and self.streamdetails.stream_title: - self.name = self.streamdetails.stream_title - if not self.name: - self.name = self.uri - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # Exclude internal streamdetails fields from dict - if streamdetails := d.get("streamdetails"): - streamdetails.pop("data", None) - streamdetails.pop("direct", None) - streamdetails.pop("expires", None) - streamdetails.pop("path", None) - streamdetails.pop("decryption_key", None) - return d - - @property - def uri(self) -> str: - """Return uri for this QueueItem (for logging purposes).""" - if self.media_item and self.media_item.uri: - return self.media_item.uri - return self.queue_item_id - - @property - def media_type(self) -> MediaType: - """Return MediaType for this QueueItem (for convenience purposes).""" - if self.media_item: - return self.media_item.media_type - if self.streamdetails: - return self.streamdetails.media_type - return MediaType.UNKNOWN - - @classmethod - def from_media_item(cls, queue_id: str, media_item: Track | Radio) -> QueueItem: - """Construct QueueItem from track/radio item.""" - if is_track(media_item): - artists = "/".join(x.name for x in media_item.artists) - name = f"{artists} - {media_item.name}" - if media_item.version: - name = f"{name} ({media_item.version})" - # save a lot of data/bandwidth by simplifying nested objects - media_item.artists = UniqueList([ItemMapping.from_item(x) for x in media_item.artists]) - if media_item.album: - media_item.album = ItemMapping.from_item(media_item.album) - else: - name = media_item.name - return cls( - queue_id=queue_id, - queue_item_id=uuid4().hex, - name=name, - duration=media_item.duration, - media_item=media_item, - image=get_image(media_item), - ) - - def to_cache(self) -> dict[str, Any]: - """Return the dict that is suitable for storing into the cache db.""" - base = self.to_dict() - base.pop("streamdetails", None) - return base - - @classmethod - def from_cache(cls, d: dict[Any, Any]) -> Self: - """Restore a QueueItem from a cache dict.""" - d.pop("streamdetails", None) - return cls.from_dict(d) - - -def get_image(media_item: Track | Radio | None) -> MediaItemImage | None: - """Find the Image for the MediaItem.""" - if not media_item: - return None - if media_item.image: - return media_item.image - if media_item.media_type == MediaType.TRACK and (album := getattr(media_item, "album", None)): - return get_image(album) - return None diff --git a/music_assistant/common/models/streamdetails.py b/music_assistant/common/models/streamdetails.py deleted file mode 100644 index 9740e9ce..00000000 --- a/music_assistant/common/models/streamdetails.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Model(s) for streamdetails.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.enums import MediaType, StreamType, VolumeNormalizationMode -from music_assistant.common.models.media_items import AudioFormat - - -@dataclass(kw_only=True) -class StreamDetails(DataClassDictMixin): - """Model for streamdetails.""" - - # NOTE: the actual provider/itemid of the streamdetails may differ - # from the connected media_item due to track linking etc. - # the streamdetails are only used to provide details about the content - # that is going to be streamed. - - # mandatory fields - provider: str - item_id: str - audio_format: AudioFormat - media_type: MediaType = MediaType.TRACK - stream_type: StreamType = StreamType.CUSTOM - path: str | None = None - decryption_key: str | None = None - - # stream_title: radio streams can optionally set this field - stream_title: str | None = None - # duration of the item to stream, copied from media_item if omitted - duration: int | None = None - # total size in bytes of the item, calculated at eof when omitted - size: int | None = None - # data: provider specific data (not exposed externally) - # this info is for example used to pass details to the get_audio_stream - data: Any = None - # can_seek: bool to indicate that the providers 'get_audio_stream' supports seeking of the item - can_seek: bool = True - - # the fields below will be set/controlled by the streamcontroller - seek_position: int = 0 - fade_in: bool = False - loudness: float | None = None - loudness_album: float | None = None - prefer_album_loudness: bool = False - volume_normalization_mode: VolumeNormalizationMode | None = None - queue_id: str | None = None - seconds_streamed: float | None = None - target_loudness: float | None = None - strip_silence_begin: bool = False - strip_silence_end: bool = False - stream_error: bool | None = None - - def __str__(self) -> str: - """Return pretty printable string of object.""" - return self.uri - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - d.pop("queue_id", None) - d.pop("seconds_streamed", None) - d.pop("seek_position", None) - d.pop("fade_in", None) - return d - - @property - def uri(self) -> str: - """Return uri representation of item.""" - return f"{self.provider}://{self.media_type.value}/{self.item_id}" diff --git a/music_assistant/constants.py b/music_assistant/constants.py index b78e4c84..57e53c01 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -3,6 +3,8 @@ import pathlib from typing import Final +from music_assistant_models.config_entries import ConfigEntry, ConfigEntryType, ConfigValueOption + API_SCHEMA_VERSION: Final[int] = 26 MIN_SCHEMA_VERSION: Final[int] = 24 @@ -24,8 +26,6 @@ SILENCE_FILE: Final[str] = str(RESOURCES_DIR.joinpath("silence.mp3")) VARIOUS_ARTISTS_FANART: Final[str] = str(RESOURCES_DIR.joinpath("fallback_fanart.jpeg")) MASS_LOGO: Final[str] = str(RESOURCES_DIR.joinpath("logo.png")) -# if duration is None (e.g. radio stream):Final[str] = 48 hours -FALLBACK_DURATION: Final[int] = 172800 # config keys CONF_SERVER_ID: Final[str] = "server_id" @@ -98,7 +98,6 @@ MASS_LOGO_ONLINE: Final[str] = ( "https://github.com/home-assistant/brands/raw/master/custom_integrations/mass/icon%402x.png" ) ENCRYPT_SUFFIX = "_encrypted_" -SECURE_STRING_SUBSTITUTE = "this_value_is_encrypted" CONFIGURABLE_CORE_CONTROLLERS = ( "streams", "webserver", @@ -110,3 +109,363 @@ CONFIGURABLE_CORE_CONTROLLERS = ( ) VERBOSE_LOG_LEVEL: Final[int] = 5 PROVIDERS_WITH_SHAREABLE_URLS = ("spotify", "qobuz") + + +####### REUSABLE CONFIG ENTRIES ####### + +CONF_ENTRY_LOG_LEVEL = ConfigEntry( + key=CONF_LOG_LEVEL, + type=ConfigEntryType.STRING, + label="Log level", + options=( + ConfigValueOption("global", "GLOBAL"), + ConfigValueOption("info", "INFO"), + ConfigValueOption("warning", "WARNING"), + ConfigValueOption("error", "ERROR"), + ConfigValueOption("debug", "DEBUG"), + ConfigValueOption("verbose", "VERBOSE"), + ), + default_value="GLOBAL", + category="advanced", +) + +DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) +DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) + +# some reusable player config entries + +CONF_ENTRY_FLOW_MODE = ConfigEntry( + key=CONF_FLOW_MODE, + type=ConfigEntryType.BOOLEAN, + label="Enable queue flow mode", + default_value=False, +) + +CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True} +) + +CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True} +) + +CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True} +) + + +CONF_ENTRY_AUTO_PLAY = ConfigEntry( + key=CONF_AUTO_PLAY, + type=ConfigEntryType.BOOLEAN, + label="Automatically play/resume on power on", + default_value=False, + description="When this player is turned ON, automatically start playing " + "(if there are items in the queue).", +) + +CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry( + key=CONF_OUTPUT_CHANNELS, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Stereo (both channels)", "stereo"), + ConfigValueOption("Left channel", "left"), + ConfigValueOption("Right channel", "right"), + ConfigValueOption("Mono (both channels)", "mono"), + ), + default_value="stereo", + label="Output Channel Mode", + category="audio", +) + +CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry( + key=CONF_VOLUME_NORMALIZATION, + type=ConfigEntryType.BOOLEAN, + label="Enable volume normalization", + default_value=True, + description="Enable volume normalization (EBU-R128 based)", + category="audio", +) + +CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_TARGET, + type=ConfigEntryType.INTEGER, + range=(-70, -5), + default_value=-17, + label="Target level for volume normalization", + description="Adjust average (perceived) loudness to this target level", + depends_on=CONF_VOLUME_NORMALIZATION, + category="advanced", +) + +CONF_ENTRY_EQ_BASS = ConfigEntry( + key=CONF_EQ_BASS, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: bass", + description="Use the builtin basic equalizer to adjust the bass of audio.", + category="audio", +) + +CONF_ENTRY_EQ_MID = ConfigEntry( + key=CONF_EQ_MID, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: midrange", + description="Use the builtin basic equalizer to adjust the midrange of audio.", + category="audio", +) + +CONF_ENTRY_EQ_TREBLE = ConfigEntry( + key=CONF_EQ_TREBLE, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: treble", + description="Use the builtin basic equalizer to adjust the treble of audio.", + category="audio", +) + + +CONF_ENTRY_CROSSFADE = ConfigEntry( + key=CONF_CROSSFADE, + type=ConfigEntryType.BOOLEAN, + label="Enable crossfade", + default_value=False, + description="Enable a crossfade transition between (queue) tracks.", + category="audio", +) + +CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED = ConfigEntry( + key=CONF_CROSSFADE, + type=ConfigEntryType.BOOLEAN, + label="Enable crossfade", + default_value=False, + description="Enable a crossfade transition between (queue) tracks.\n\n " + "Requires flow-mode to be enabled", + category="audio", + depends_on=CONF_FLOW_MODE, +) + +CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry( + key=CONF_CROSSFADE_DURATION, + type=ConfigEntryType.INTEGER, + range=(1, 10), + default_value=8, + label="Crossfade duration", + description="Duration in seconds of the crossfade between tracks (if enabled)", + depends_on=CONF_CROSSFADE, + category="advanced", +) + +CONF_ENTRY_HIDE_PLAYER = ConfigEntry( + key=CONF_HIDE_PLAYER, + type=ConfigEntryType.BOOLEAN, + label="Hide this player in the user interface", + default_value=False, +) + +CONF_ENTRY_ENFORCE_MP3 = ConfigEntry( + key=CONF_ENFORCE_MP3, + type=ConfigEntryType.BOOLEAN, + label="Enforce (lossy) mp3 stream", + default_value=False, + description="By default, Music Assistant sends lossless, high quality audio " + "to all players. Some players can not deal with that and require the stream to be packed " + "into a lossy mp3 codec. \n\n " + "Only enable when needed. Saves some bandwidth at the cost of audio quality.", + category="audio", +) + +CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True} +) + +CONF_ENTRY_SYNC_ADJUST = ConfigEntry( + key=CONF_SYNC_ADJUST, + type=ConfigEntryType.INTEGER, + range=(-500, 500), + default_value=0, + label="Audio synchronization delay correction", + description="If this player is playing audio synced with other players " + "and you always hear the audio too early or late on this player, " + "you can shift the audio a bit.", + category="advanced", +) + + +CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry( + key=CONF_TTS_PRE_ANNOUNCE, + type=ConfigEntryType.BOOLEAN, + default_value=True, + label="Pre-announce TTS announcements", + category="announcements", +) + + +CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_STRATEGY, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Absolute volume", "absolute"), + ConfigValueOption("Relative volume increase", "relative"), + ConfigValueOption("Volume increase by fixed percentage", "percentual"), + ConfigValueOption("Do not adjust volume", "none"), + ), + default_value="percentual", + label="Volume strategy for Announcements", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME, + type=ConfigEntryType.INTEGER, + default_value=85, + label="Volume for Announcements", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_MIN, + type=ConfigEntryType.INTEGER, + default_value=15, + label="Minimum Volume level for Announcements", + description="The volume (adjustment) of announcements should no go below this level.", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_MAX, + type=ConfigEntryType.INTEGER, + default_value=75, + label="Maximum Volume level for Announcements", + description="The volume (adjustment) of announcements should no go above this level.", + category="announcements", +) + +CONF_ENTRY_PLAYER_ICON = ConfigEntry( + key=CONF_ICON, + type=ConfigEntryType.ICON, + default_value="mdi-speaker", + label="Icon", + description="Material design icon for this player. " + "\n\nSee https://pictogrammers.com/library/mdi/", + category="generic", +) + +CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict( + {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"} +) + +CONF_ENTRY_SAMPLE_RATES = ConfigEntry( + key=CONF_SAMPLE_RATES, + type=ConfigEntryType.INTEGER_TUPLE, + options=( + ConfigValueOption("44.1kHz / 16 bits", (44100, 16)), + ConfigValueOption("44.1kHz / 24 bits", (44100, 24)), + ConfigValueOption("48kHz / 16 bits", (48000, 16)), + ConfigValueOption("48kHz / 24 bits", (48000, 24)), + ConfigValueOption("88.2kHz / 16 bits", (88200, 16)), + ConfigValueOption("88.2kHz / 24 bits", (88200, 24)), + ConfigValueOption("96kHz / 16 bits", (96000, 16)), + ConfigValueOption("96kHz / 24 bits", (96000, 24)), + ConfigValueOption("176.4kHz / 16 bits", (176400, 16)), + ConfigValueOption("176.4kHz / 24 bits", (176400, 24)), + ConfigValueOption("192kHz / 16 bits", (192000, 16)), + ConfigValueOption("192kHz / 24 bits", (192000, 24)), + ConfigValueOption("352.8kHz / 16 bits", (352800, 16)), + ConfigValueOption("352.8kHz / 24 bits", (352800, 24)), + ConfigValueOption("384kHz / 16 bits", (384000, 16)), + ConfigValueOption("384kHz / 24 bits", (384000, 24)), + ), + default_value=[(44100, 16), (48000, 16)], + required=True, + multi_value=True, + label="Sample rates supported by this player", + category="advanced", + description="The sample rates (and bit depths) supported by this player.\n" + "Content with unsupported sample rates will be automatically resampled.", +) + + +CONF_ENTRY_HTTP_PROFILE = ConfigEntry( + key=CONF_HTTP_PROFILE, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Profile 1 - chunked", "chunked"), + ConfigValueOption("Profile 2 - no content length", "no_content_length"), + ConfigValueOption("Profile 3 - forced content length", "forced_content_length"), + ), + default_value="no_content_length", + label="HTTP Profile used for sending audio", + category="advanced", + description="This is considered to be a very advanced setting, only adjust this if needed, " + "for example if your player stops playing halfway streams or if you experience " + "other playback related issues. In most cases the default setting is fine.", +) + +CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict( + {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked", "hidden": True} +) +CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict( + {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length", "hidden": True} +) + +CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry( + key=CONF_ENABLE_ICY_METADATA, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Disabled - do not send ICY metadata", "disabled"), + ConfigValueOption("Profile 1 - basic info", "basic"), + ConfigValueOption("Profile 2 - full info (including image)", "full"), + ), + depends_on=CONF_FLOW_MODE, + default_value="disabled", + label="Try to ingest metadata into stream (ICY)", + category="advanced", + description="Try to ingest metadata into the stream (ICY) to show track info on the player, " + "even when flow mode is enabled.\n\nThis is called ICY metadata and its what is also used by " + "online radio station to inform you what is playing. \n\nBe aware that not all players support " + "this correctly. If you experience issues with playback, try to disable this setting.", +) + + +def create_sample_rates_config_entry( + max_sample_rate: int, + max_bit_depth: int, + safe_max_sample_rate: int = 48000, + safe_max_bit_depth: int = 16, + hidden: bool = False, +) -> ConfigEntry: + """Create sample rates config entry based on player specific helpers.""" + assert CONF_ENTRY_SAMPLE_RATES.options + conf_entry = ConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict()) + conf_entry.hidden = hidden + options: list[ConfigValueOption] = [] + default_value: list[tuple[int, int]] = [] + for option in CONF_ENTRY_SAMPLE_RATES.options: + if not isinstance(option.value, tuple): + continue + sample_rate, bit_depth = option.value + if sample_rate <= max_sample_rate and bit_depth <= max_bit_depth: + options.append(option) + if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth: + default_value.append(option.value) + conf_entry.options = tuple(options) + conf_entry.default_value = default_value + return conf_entry + + +BASE_PLAYER_CONFIG_ENTRIES = ( + # config entries that are valid for all players + CONF_ENTRY_PLAYER_ICON, + CONF_ENTRY_FLOW_MODE, + CONF_ENTRY_VOLUME_NORMALIZATION, + CONF_ENTRY_AUTO_PLAY, + CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, + CONF_ENTRY_HIDE_PLAYER, + CONF_ENTRY_TTS_PRE_ANNOUNCE, + CONF_ENTRY_SAMPLE_RATES, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, +) diff --git a/music_assistant/controllers/__init__.py b/music_assistant/controllers/__init__.py new file mode 100644 index 00000000..49fe05d5 --- /dev/null +++ b/music_assistant/controllers/__init__.py @@ -0,0 +1 @@ +"""Package with controllers.""" diff --git a/music_assistant/controllers/cache.py b/music_assistant/controllers/cache.py new file mode 100644 index 00000000..687b3374 --- /dev/null +++ b/music_assistant/controllers/cache.py @@ -0,0 +1,408 @@ +"""Provides a simple stateless caching system.""" + +from __future__ import annotations + +import asyncio +import functools +import logging +import os +import time +from collections import OrderedDict +from collections.abc import Callable, Iterator, MutableMapping +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType + +from music_assistant.constants import DB_TABLE_CACHE, DB_TABLE_SETTINGS, MASS_LOGGER_NAME +from music_assistant.helpers.database import DatabaseConnection +from music_assistant.helpers.json import json_dumps, json_loads +from music_assistant.models.core_controller import CoreController + +if TYPE_CHECKING: + from music_assistant_models.config_entries import CoreConfig + +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.cache") +CONF_CLEAR_CACHE = "clear_cache" +DB_SCHEMA_VERSION = 5 + + +class CacheController(CoreController): + """Basic cache controller using both memory and database.""" + + domain: str = "cache" + + def __init__(self, *args, **kwargs) -> None: + """Initialize core controller.""" + super().__init__(*args, **kwargs) + self.database: DatabaseConnection | None = None + self._mem_cache = MemoryCache(500) + self.manifest.name = "Cache controller" + self.manifest.description = ( + "Music Assistant's core controller for caching data throughout the application." + ) + self.manifest.icon = "memory" + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + if action == CONF_CLEAR_CACHE: + await self.clear() + return ( + ConfigEntry( + key=CONF_CLEAR_CACHE, + type=ConfigEntryType.LABEL, + label="The cache has been cleared", + ), + ) + return ( + ConfigEntry( + key=CONF_CLEAR_CACHE, + type=ConfigEntryType.ACTION, + label="Clear cache", + description="Reset/clear all items in the cache. ", + ), + ) + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of cache module.""" + self.logger.info("Initializing cache controller...") + await self._setup_database() + self.__schedule_cleanup_task() + + async def close(self) -> None: + """Cleanup on exit.""" + await self.database.close() + + async def get( + self, + key: str, + checksum: str | None = None, + default=None, + category: int = 0, + base_key: str = "", + ) -> Any: + """Get object from cache and return the results. + + cache_key: the (unique) name of the cache object as reference + checksum: optional argument to check if the checksum in the + cacheobject matches the checksum provided + category: optional category to group cache objects + base_key: optional base key to group cache objects + """ + if not key: + return None + cur_time = int(time.time()) + if checksum is not None and not isinstance(checksum, str): + checksum = str(checksum) + + # try memory cache first + memory_key = f"{category}/{base_key}/{key}" + cache_data = self._mem_cache.get(memory_key) + if cache_data and (not checksum or cache_data[1] == checksum) and cache_data[2] >= cur_time: + return cache_data[0] + # fall back to db cache + if ( + db_row := await self.database.get_row( + DB_TABLE_CACHE, {"category": category, "base_key": base_key, "sub_key": key} + ) + ) and (not checksum or db_row["checksum"] == checksum and db_row["expires"] >= cur_time): + try: + data = await asyncio.to_thread(json_loads, db_row["data"]) + except Exception as exc: + LOGGER.error( + "Error parsing cache data for %s: %s", + memory_key, + str(exc), + exc_info=exc if self.logger.isEnabledFor(10) else None, + ) + else: + # also store in memory cache for faster access + self._mem_cache[memory_key] = ( + data, + db_row["checksum"], + db_row["expires"], + ) + return data + return default + + async def set( + self, key, data, checksum="", expiration=(86400 * 7), category: int = 0, base_key: str = "" + ) -> None: + """Set data in cache.""" + if not key: + return + if checksum is not None and not isinstance(checksum, str): + checksum = str(checksum) + expires = int(time.time() + expiration) + memory_key = f"{category}/{base_key}/{key}" + self._mem_cache[memory_key] = (data, checksum, expires) + if (expires - time.time()) < 3600 * 12: + # do not cache items in db with short expiration + return + data = await asyncio.to_thread(json_dumps, data) + await self.database.insert_or_replace( + DB_TABLE_CACHE, + { + "category": category, + "base_key": base_key, + "sub_key": key, + "expires": expires, + "checksum": checksum, + "data": data, + }, + ) + + async def delete( + self, key: str | None, category: int | None = None, base_key: str | None = None + ) -> None: + """Delete data from cache.""" + match: dict[str, str | int] = {} + if key is not None: + match["sub_key"] = key + if category is not None: + match["category"] = category + if base_key is not None: + match["base_key"] = base_key + if key is not None and category is not None and base_key is not None: + self._mem_cache.pop(f"{category}/{base_key}/{key}", None) + else: + self._mem_cache.clear() + await self.database.delete(DB_TABLE_CACHE, match) + + async def clear( + self, + key_filter: str | None = None, + category: int | None = None, + base_key_filter: str | None = None, + ) -> None: + """Clear all/partial items from cache.""" + self._mem_cache.clear() + self.logger.info("Clearing database...") + query_parts: list[str] = [] + if category is not None: + query_parts.append(f"category = {category}") + if base_key_filter is not None: + query_parts.append(f"base_key LIKE '%{base_key_filter}%'") + if key_filter is not None: + query_parts.append(f"sub_key LIKE '%{key_filter}%'") + query = "WHERE " + " AND ".join(query_parts) if query_parts else None + await self.database.delete(DB_TABLE_CACHE, query=query) + self.logger.info("Clearing database DONE") + + async def auto_cleanup(self) -> None: + """Run scheduled auto cleanup task.""" + self.logger.debug("Running automatic cleanup...") + # simply reset the memory cache + self._mem_cache.clear() + cur_timestamp = int(time.time()) + cleaned_records = 0 + for db_row in await self.database.get_rows(DB_TABLE_CACHE): + # clean up db cache object only if expired + if db_row["expires"] < cur_timestamp: + await self.database.delete(DB_TABLE_CACHE, {"id": db_row["id"]}) + cleaned_records += 1 + await asyncio.sleep(0) # yield to eventloop + self.logger.debug("Automatic cleanup finished (cleaned up %s records)", cleaned_records) + + async def _setup_database(self) -> None: + """Initialize database.""" + db_path = os.path.join(self.mass.storage_path, "cache.db") + self.database = DatabaseConnection(db_path) + await self.database.setup() + + # always create db tables if they don't exist to prevent errors trying to access them later + await self.__create_database_tables() + try: + if db_row := await self.database.get_row(DB_TABLE_SETTINGS, {"key": "version"}): + prev_version = int(db_row["value"]) + else: + prev_version = 0 + except (KeyError, ValueError): + prev_version = 0 + + if prev_version not in (0, DB_SCHEMA_VERSION): + LOGGER.warning( + "Performing database migration from %s to %s", + prev_version, + DB_SCHEMA_VERSION, + ) + + if prev_version < DB_SCHEMA_VERSION: + # for now just keep it simple and just recreate the table(s) + await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_CACHE}") + + # recreate missing table(s) + await self.__create_database_tables() + + # store current schema version + await self.database.insert_or_replace( + DB_TABLE_SETTINGS, + {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"}, + ) + await self.__create_database_indexes() + # compact db (vacuum) at startup + self.logger.debug("Compacting database...") + try: + await self.database.vacuum() + except Exception as err: + self.logger.warning("Database vacuum failed: %s", str(err)) + else: + self.logger.debug("Compacting database done") + + async def __create_database_tables(self) -> None: + """Create database table(s).""" + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_SETTINGS}( + key TEXT PRIMARY KEY, + value TEXT, + type TEXT + );""" + ) + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_CACHE}( + [id] INTEGER PRIMARY KEY AUTOINCREMENT, + [category] INTEGER NOT NULL DEFAULT 0, + [base_key] TEXT NOT NULL, + [sub_key] TEXT NOT NULL, + [expires] INTEGER NOT NULL, + [data] TEXT, + [checksum] TEXT NULL, + UNIQUE(category, base_key, sub_key) + )""" + ) + + await self.database.commit() + + async def __create_database_indexes(self) -> None: + """Create database indexes.""" + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_idx " + f"ON {DB_TABLE_CACHE}(category);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_base_key_idx " + f"ON {DB_TABLE_CACHE}(base_key);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_sub_key_idx " + f"ON {DB_TABLE_CACHE}(sub_key);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_base_key_idx " + f"ON {DB_TABLE_CACHE}(category,base_key);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_base_key_sub_key_idx " + f"ON {DB_TABLE_CACHE}(category,base_key,sub_key);" + ) + await self.database.commit() + + def __schedule_cleanup_task(self) -> None: + """Schedule the cleanup task.""" + self.mass.create_task(self.auto_cleanup()) + # reschedule self + self.mass.loop.call_later(3600, self.__schedule_cleanup_task) + + +Param = ParamSpec("Param") +RetType = TypeVar("RetType") + + +def use_cache( + expiration: int = 86400 * 30, + category: int = 0, +) -> Callable[[Callable[Param, RetType]], Callable[Param, RetType]]: + """Return decorator that can be used to cache a method's result.""" + + def wrapper(func: Callable[Param, RetType]) -> Callable[Param, RetType]: + @functools.wraps(func) + async def wrapped(*args: Param.args, **kwargs: Param.kwargs): + method_class = args[0] + method_class_name = method_class.__class__.__name__ + cache_base_key = f"{method_class_name}.{func.__name__}" + cache_sub_key_parts = [] + skip_cache = kwargs.pop("skip_cache", False) + cache_checksum = kwargs.pop("cache_checksum", "") + if len(args) > 1: + cache_sub_key_parts += args[1:] + for key in sorted(kwargs.keys()): + cache_sub_key_parts.append(f"{key}{kwargs[key]}") + cache_sub_key = ".".join(cache_sub_key_parts) + + cachedata = await method_class.cache.get( + cache_sub_key, checksum=cache_checksum, category=category, base_key=cache_base_key + ) + + if not skip_cache and cachedata is not None: + return cachedata + result = await func(*args, **kwargs) + asyncio.create_task( + method_class.cache.set( + cache_sub_key, + result, + expiration=expiration, + checksum=cache_checksum, + category=category, + base_key=cache_base_key, + ) + ) + return result + + return wrapped + + return wrapper + + +class MemoryCache(MutableMapping): + """Simple limited in-memory cache implementation.""" + + def __init__(self, maxlen: int) -> None: + """Initialize.""" + self._maxlen = maxlen + self.d = OrderedDict() + + @property + def maxlen(self) -> int: + """Return max length.""" + return self._maxlen + + def get(self, key: str, default: Any = None) -> Any: + """Return item or default.""" + return self.d.get(key, default) + + def pop(self, key: str, default: Any = None) -> Any: + """Pop item from collection.""" + return self.d.pop(key, default) + + def __getitem__(self, key: str) -> Any: + """Get item.""" + self.d.move_to_end(key) + return self.d[key] + + def __setitem__(self, key: str, value: Any) -> None: + """Set item.""" + if key in self.d: + self.d.move_to_end(key) + elif len(self.d) == self.maxlen: + self.d.popitem(last=False) + self.d[key] = value + + def __delitem__(self, key) -> None: + """Delete item.""" + del self.d[key] + + def __iter__(self) -> Iterator: + """Iterate items.""" + return self.d.__iter__() + + def __len__(self) -> int: + """Return length.""" + return len(self.d) + + def clear(self) -> None: + """Clear cache.""" + self.d.clear() diff --git a/music_assistant/controllers/config.py b/music_assistant/controllers/config.py new file mode 100644 index 00000000..f8bfea0c --- /dev/null +++ b/music_assistant/controllers/config.py @@ -0,0 +1,841 @@ +"""Logic to handle storage of persistent (configuration) settings.""" + +from __future__ import annotations + +import base64 +import logging +import os +from contextlib import suppress +from typing import TYPE_CHECKING, Any +from uuid import uuid4 + +import aiofiles +import shortuuid +from aiofiles.os import wrap +from cryptography.fernet import Fernet, InvalidToken +from music_assistant_models import config_entries +from music_assistant_models.config_entries import ( + DEFAULT_CORE_CONFIG_ENTRIES, + DEFAULT_PROVIDER_CONFIG_ENTRIES, + ConfigEntry, + ConfigValueType, + CoreConfig, + PlayerConfig, + ProviderConfig, +) +from music_assistant_models.enums import EventType, ProviderFeature, ProviderType +from music_assistant_models.errors import ( + ActionUnavailable, + InvalidDataError, + PlayerCommandFailed, + UnsupportedFeaturedException, +) +from music_assistant_models.helpers.global_cache import get_global_cache_value + +from music_assistant.constants import ( + CONF_CORE, + CONF_PLAYERS, + CONF_PROVIDERS, + CONF_SERVER_ID, + CONFIGURABLE_CORE_CONTROLLERS, + ENCRYPT_SUFFIX, +) +from music_assistant.helpers.api import api_command +from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads +from music_assistant.helpers.util import load_provider_module + +if TYPE_CHECKING: + import asyncio + + from music_assistant import MusicAssistant + from music_assistant.models.core_controller import CoreController + +LOGGER = logging.getLogger(__name__) +DEFAULT_SAVE_DELAY = 5 + +BASE_KEYS = ("enabled", "name", "available", "default_name", "provider", "type") + +isfile = wrap(os.path.isfile) +remove = wrap(os.remove) +rename = wrap(os.rename) + + +class ConfigController: + """Controller that handles storage of persistent configuration settings.""" + + _fernet: Fernet | None = None + + def __init__(self, mass: MusicAssistant) -> None: + """Initialize storage controller.""" + self.mass = mass + self.initialized = False + self._data: dict[str, Any] = {} + self.filename = os.path.join(self.mass.storage_path, "settings.json") + self._timer_handle: asyncio.TimerHandle | None = None + self._value_cache: dict[str, ConfigValueType] = {} + + async def setup(self) -> None: + """Async initialize of controller.""" + await self._load() + self.initialized = True + # create default server ID if needed (also used for encrypting passwords) + self.set_default(CONF_SERVER_ID, uuid4().hex) + server_id: str = self.get(CONF_SERVER_ID) + assert server_id + fernet_key = base64.urlsafe_b64encode(server_id.encode()[:32]) + self._fernet = Fernet(fernet_key) + config_entries.ENCRYPT_CALLBACK = self.encrypt_string + config_entries.DECRYPT_CALLBACK = self.decrypt_string + LOGGER.debug("Started.") + + @property + def onboard_done(self) -> bool: + """Return True if onboarding is done.""" + return len(self._data.get(CONF_PROVIDERS, {})) > 0 + + async def close(self) -> None: + """Handle logic on server stop.""" + if not self._timer_handle: + # no point in forcing a save when there are no changes pending + return + await self._async_save() + LOGGER.debug("Stopped.") + + def get(self, key: str, default: Any = None) -> Any: + """Get value(s) for a specific key/path in persistent storage.""" + assert self.initialized, "Not yet (async) initialized" + # we support a multi level hierarchy by providing the key as path, + # with a slash (/) as splitter. Sort that out here. + parent = self._data + subkeys = key.split("/") + for index, subkey in enumerate(subkeys): + if index == (len(subkeys) - 1): + value = parent.get(subkey, default) + if value is None: + # replace None with default + return default + return value + if subkey not in parent: + # requesting subkey from a non existing parent + return default + parent = parent[subkey] + return default + + def set(self, key: str, value: Any) -> None: + """Set value(s) for a specific key/path in persistent storage.""" + assert self.initialized, "Not yet (async) initialized" + # we support a multi level hierarchy by providing the key as path, + # with a slash (/) as splitter. + parent = self._data + subkeys = key.split("/") + for index, subkey in enumerate(subkeys): + if index == (len(subkeys) - 1): + parent[subkey] = value + else: + parent.setdefault(subkey, {}) + parent = parent[subkey] + self.save() + + def set_default(self, key: str, default_value: Any) -> None: + """Set default value(s) for a specific key/path in persistent storage.""" + assert self.initialized, "Not yet (async) initialized" + cur_value = self.get(key, "__MISSING__") + if cur_value == "__MISSING__": + self.set(key, default_value) + + def remove( + self, + key: str, + ) -> None: + """Remove value(s) for a specific key/path in persistent storage.""" + assert self.initialized, "Not yet (async) initialized" + parent = self._data + subkeys = key.split("/") + for index, subkey in enumerate(subkeys): + if subkey not in parent: + return + if index == (len(subkeys) - 1): + parent.pop(subkey) + else: + parent.setdefault(subkey, {}) + parent = parent[subkey] + + self.save() + + @api_command("config/providers") + async def get_provider_configs( + self, + provider_type: ProviderType | None = None, + provider_domain: str | None = None, + include_values: bool = False, + ) -> list[ProviderConfig]: + """Return all known provider configurations, optionally filtered by ProviderType.""" + raw_values: dict[str, dict] = self.get(CONF_PROVIDERS, {}) + prov_entries = {x.domain for x in self.mass.get_provider_manifests()} + return [ + await self.get_provider_config(prov_conf["instance_id"]) + if include_values + else ProviderConfig.parse([], prov_conf) + for prov_conf in raw_values.values() + if (provider_type is None or prov_conf["type"] == provider_type) + and (provider_domain is None or prov_conf["domain"] == provider_domain) + # guard for deleted providers + and prov_conf["domain"] in prov_entries + ] + + @api_command("config/providers/get") + async def get_provider_config(self, instance_id: str) -> ProviderConfig: + """Return configuration for a single provider.""" + if raw_conf := self.get(f"{CONF_PROVIDERS}/{instance_id}", {}): + config_entries = await self.get_provider_config_entries( + raw_conf["domain"], + instance_id=instance_id, + values=raw_conf.get("values"), + ) + for prov in self.mass.get_provider_manifests(): + if prov.domain == raw_conf["domain"]: + break + else: + msg = f'Unknown provider domain: {raw_conf["domain"]}' + raise KeyError(msg) + return ProviderConfig.parse(config_entries, raw_conf) + msg = f"No config found for provider id {instance_id}" + raise KeyError(msg) + + @api_command("config/providers/get_value") + async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueType: + """Return single configentry value for a provider.""" + cache_key = f"prov_conf_value_{instance_id}.{key}" + if (cached_value := self._value_cache.get(cache_key)) is not None: + return cached_value + conf = await self.get_provider_config(instance_id) + val = ( + conf.values[key].value + if conf.values[key].value is not None + else conf.values[key].default_value + ) + # store value in cache because this method can potentially be called very often + self._value_cache[cache_key] = val + return val + + @api_command("config/providers/get_entries") + async def get_provider_config_entries( + self, + provider_domain: str, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup/configure a provider. + + provider_domain: (mandatory) domain of the provider. + instance_id: id of an existing provider instance (None for new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # lookup provider manifest and module + for prov in self.mass.get_provider_manifests(): + if prov.domain == provider_domain: + prov_mod = await load_provider_module(provider_domain, prov.requirements) + break + else: + msg = f"Unknown provider domain: {provider_domain}" + raise KeyError(msg) + if values is None: + values = self.get(f"{CONF_PROVIDERS}/{instance_id}/values", {}) if instance_id else {} + + return ( + await prov_mod.get_config_entries( + self.mass, instance_id=instance_id, action=action, values=values + ) + + DEFAULT_PROVIDER_CONFIG_ENTRIES + ) + + @api_command("config/providers/save") + async def save_provider_config( + self, + provider_domain: str, + values: dict[str, ConfigValueType], + instance_id: str | None = None, + ) -> ProviderConfig: + """ + Save Provider(instance) Config. + + provider_domain: (mandatory) domain of the provider. + values: the raw values for config entries that need to be stored/updated. + instance_id: id of an existing provider instance (None for new instance setup). + """ + if instance_id is not None: + config = await self._update_provider_config(instance_id, values) + else: + config = await self._add_provider_config(provider_domain, values) + # return full config, just in case + return await self.get_provider_config(config.instance_id) + + @api_command("config/providers/remove") + async def remove_provider_config(self, instance_id: str) -> None: + """Remove ProviderConfig.""" + conf_key = f"{CONF_PROVIDERS}/{instance_id}" + existing = self.get(conf_key) + if not existing: + msg = f"Provider {instance_id} does not exist" + raise KeyError(msg) + prov_manifest = self.mass.get_provider_manifest(existing["domain"]) + if prov_manifest.builtin: + msg = f"Builtin provider {prov_manifest.name} can not be removed." + raise RuntimeError(msg) + self.remove(conf_key) + await self.mass.unload_provider(instance_id) + if existing["type"] == "music": + # cleanup entries in library + await self.mass.music.cleanup_provider(instance_id) + if existing["type"] == "player": + # cleanup entries in player manager + for player in list(self.mass.players): + if player.provider != instance_id: + continue + self.mass.players.remove(player.player_id, cleanup_config=True) + + async def remove_provider_config_value(self, instance_id: str, key: str) -> None: + """Remove/reset single Provider config value.""" + conf_key = f"{CONF_PROVIDERS}/{instance_id}/values/{key}" + existing = self.get(conf_key) + if not existing: + return + self.remove(conf_key) + + @api_command("config/players") + async def get_player_configs( + self, provider: str | None = None, include_values: bool = False + ) -> list[PlayerConfig]: + """Return all known player configurations, optionally filtered by provider domain.""" + return [ + await self.get_player_config(raw_conf["player_id"]) + if include_values + else PlayerConfig.parse([], raw_conf) + for raw_conf in list(self.get(CONF_PLAYERS, {}).values()) + # filter out unavailable providers (only if we requested the full info) + if ( + not include_values + or raw_conf["provider"] in get_global_cache_value("available_providers", []) + ) + # optional provider filter + and (provider in (None, raw_conf["provider"])) + ] + + @api_command("config/players/get") + async def get_player_config(self, player_id: str) -> PlayerConfig: + """Return (full) configuration for a single player.""" + if raw_conf := self.get(f"{CONF_PLAYERS}/{player_id}"): + if player := self.mass.players.get(player_id, False): + raw_conf["default_name"] = player.display_name + raw_conf["provider"] = player.provider + prov = self.mass.get_provider(player.provider) + conf_entries = await prov.get_player_config_entries(player_id) + else: + # handle unavailable player and/or provider + if prov := self.mass.get_provider(raw_conf["provider"]): + conf_entries = await prov.get_player_config_entries(player_id) + else: + conf_entries = () + raw_conf["available"] = False + raw_conf["name"] = raw_conf.get("name") + raw_conf["default_name"] = raw_conf.get("default_name") or raw_conf["player_id"] + return PlayerConfig.parse(conf_entries, raw_conf) + msg = f"No config found for player id {player_id}" + raise KeyError(msg) + + @api_command("config/players/get_value") + async def get_player_config_value( + self, + player_id: str, + key: str, + ) -> ConfigValueType: + """Return single configentry value for a player.""" + conf = await self.get_player_config(player_id) + return ( + conf.values[key].value + if conf.values[key].value is not None + else conf.values[key].default_value + ) + + def get_raw_player_config_value( + self, player_id: str, key: str, default: ConfigValueType = None + ) -> ConfigValueType: + """ + Return (raw) single configentry value for a player. + + Note that this only returns the stored value without any validation or default. + """ + return self.get( + f"{CONF_PLAYERS}/{player_id}/values/{key}", + self.get(f"{CONF_PLAYERS}/{player_id}/{key}", default), + ) + + @api_command("config/players/save") + async def save_player_config( + self, player_id: str, values: dict[str, ConfigValueType] + ) -> PlayerConfig: + """Save/update PlayerConfig.""" + config = await self.get_player_config(player_id) + changed_keys = config.update(values) + if not changed_keys: + # no changes + return None + # validate/handle the update in the player manager + await self.mass.players.on_player_config_change(config, changed_keys) + # actually store changes (if the above did not raise) + conf_key = f"{CONF_PLAYERS}/{player_id}" + self.set(conf_key, config.to_raw()) + # send config updated event + self.mass.signal_event( + EventType.PLAYER_CONFIG_UPDATED, + object_id=config.player_id, + data=config, + ) + self.mass.players.update(config.player_id, force_update=True) + # return full player config (just in case) + return await self.get_player_config(player_id) + + @api_command("config/players/remove") + async def remove_player_config(self, player_id: str) -> None: + """Remove PlayerConfig.""" + conf_key = f"{CONF_PLAYERS}/{player_id}" + existing = self.get(conf_key) + if not existing: + msg = f"Player configuration for {player_id} does not exist" + raise KeyError(msg) + player = self.mass.players.get(player_id) + player_prov = player.provider if player else existing["provider"] + player_provider = self.mass.get_provider(player_prov) + if player_provider and ProviderFeature.REMOVE_PLAYER in player_provider.supported_features: + # provider supports removal of player (e.g. group player) + await player_provider.remove_player(player_id) + elif player and player_provider and player.available: + # removing a player config while it is active is not allowed + # unless the provider repoirts it has the remove_player feature (e.g. group player) + raise ActionUnavailable("Can not remove config for an active player!") + # check for group memberships that need to be updated + if player and player.active_group and player_provider: + # try to remove from the group + group_player = self.mass.players.get(player.active_group) + with suppress(UnsupportedFeaturedException, PlayerCommandFailed): + await player_provider.set_members( + player.active_group, + [x for x in group_player.group_childs if x != player.player_id], + ) + # tell the player manager to remove the player if its lingering around + # set cleanup_flag to false otherwise we end up in an infinite loop + self.mass.players.remove(player_id, cleanup_config=False) + # remove the actual config if all of the above passed + self.remove(conf_key) + + def create_default_player_config( + self, + player_id: str, + provider: str, + name: str, + enabled: bool, + values: dict[str, ConfigValueType] | None = None, + ) -> None: + """ + Create default/empty PlayerConfig. + + This is meant as helper to create default configs when a player is registered. + Called by the player manager on player register. + """ + # return early if the config already exists + if self.get(f"{CONF_PLAYERS}/{player_id}"): + # update default name if needed + if name: + self.set(f"{CONF_PLAYERS}/{player_id}/default_name", name) + return + # config does not yet exist, create a default one + conf_key = f"{CONF_PLAYERS}/{player_id}" + default_conf = PlayerConfig( + values={}, + provider=provider, + player_id=player_id, + enabled=enabled, + default_name=name, + ) + default_conf_raw = default_conf.to_raw() + if values is not None: + default_conf_raw["values"] = values + self.set( + conf_key, + default_conf_raw, + ) + + async def create_builtin_provider_config(self, provider_domain: str) -> None: + """ + Create builtin ProviderConfig. + + This is meant as helper to create default configs for builtin providers. + Called by the server initialization code which load all providers at startup. + """ + for _ in await self.get_provider_configs(provider_domain=provider_domain): + # return if there is already any config + return + for prov in self.mass.get_provider_manifests(): + if prov.domain == provider_domain: + manifest = prov + break + else: + msg = f"Unknown provider domain: {provider_domain}" + raise KeyError(msg) + 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": instance_id, + "name": manifest.name, + # note: this will only work for providers that do + # not have any required config entries or provide defaults + "values": {}, + }, + ) + default_config.validate() + conf_key = f"{CONF_PROVIDERS}/{default_config.instance_id}" + self.set(conf_key, default_config.to_raw()) + + @api_command("config/core") + async def get_core_configs(self, include_values: bool = False) -> list[CoreConfig]: + """Return all core controllers config options.""" + return [ + await self.get_core_config(core_controller) + if include_values + else CoreConfig.parse( + [], self.get(f"{CONF_CORE}/{core_controller}", {"domain": core_controller}) + ) + for core_controller in CONFIGURABLE_CORE_CONTROLLERS + ] + + @api_command("config/core/get") + async def get_core_config(self, domain: str) -> CoreConfig: + """Return configuration for a single core controller.""" + raw_conf = self.get(f"{CONF_CORE}/{domain}", {"domain": domain}) + config_entries = await self.get_core_config_entries(domain) + return CoreConfig.parse(config_entries, raw_conf) + + @api_command("config/core/get_value") + async def get_core_config_value(self, domain: str, key: str) -> ConfigValueType: + """Return single configentry value for a core controller.""" + conf = await self.get_core_config(domain) + return ( + conf.values[key].value + if conf.values[key].value is not None + else conf.values[key].default_value + ) + + @api_command("config/core/get_entries") + async def get_core_config_entries( + self, + domain: str, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to configure a core controller. + + core_controller: name of the core controller + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + if values is None: + values = self.get(f"{CONF_CORE}/{domain}/values", {}) + controller: CoreController = getattr(self.mass, domain) + return ( + await controller.get_config_entries(action=action, values=values) + + DEFAULT_CORE_CONFIG_ENTRIES + ) + + @api_command("config/core/save") + async def save_core_config( + self, + domain: str, + values: dict[str, ConfigValueType], + ) -> CoreConfig: + """Save CoreController Config values.""" + config = await self.get_core_config(domain) + changed_keys = config.update(values) + # validate the new config + config.validate() + if not changed_keys: + # no changes + return config + # try to load the provider first to catch errors before we save it. + controller: CoreController = getattr(self.mass, domain) + await controller.reload(config) + # reload succeeded, save new config + config.last_error = None + conf_key = f"{CONF_CORE}/{domain}" + self.set(conf_key, config.to_raw()) + # return full config, just in case + return await self.get_core_config(domain) + + def get_raw_core_config_value( + self, core_module: str, key: str, default: ConfigValueType = None + ) -> ConfigValueType: + """ + Return (raw) single configentry value for a core controller. + + Note that this only returns the stored value without any validation or default. + """ + return self.get( + f"{CONF_CORE}/{core_module}/values/{key}", + self.get(f"{CONF_CORE}/{core_module}/{key}", default), + ) + + def get_raw_provider_config_value( + self, provider_instance: str, key: str, default: ConfigValueType = None + ) -> ConfigValueType: + """ + Return (raw) single config(entry) value for a provider. + + Note that this only returns the stored value without any validation or default. + """ + return self.get( + f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", + self.get(f"{CONF_PROVIDERS}/{provider_instance}/{key}", default), + ) + + def set_raw_provider_config_value( + self, provider_instance: str, key: str, value: ConfigValueType, encrypted: bool = False + ) -> None: + """ + Set (raw) single config(entry) value for a provider. + + Note that this only stores the (raw) value without any validation or default. + """ + if not self.get(f"{CONF_PROVIDERS}/{provider_instance}"): + # only allow setting raw values if main entry exists + msg = f"Invalid provider_instance: {provider_instance}" + raise KeyError(msg) + if encrypted: + value = self.encrypt_string(value) + if key in BASE_KEYS: + self.set(f"{CONF_PROVIDERS}/{provider_instance}/{key}", value) + return + self.set(f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", value) + # also update the cached value in the provider itself + if prov := self.mass.get_provider(provider_instance, return_unavailable=True): + prov.config.values[key].value = value + + def set_raw_core_config_value(self, core_module: str, key: str, value: ConfigValueType) -> None: + """ + Set (raw) single config(entry) value for a core controller. + + Note that this only stores the (raw) value without any validation or default. + """ + if not self.get(f"{CONF_CORE}/{core_module}"): + # create base object first if needed + self.set(f"{CONF_CORE}/{core_module}", CoreConfig({}, core_module).to_raw()) + self.set(f"{CONF_CORE}/{core_module}/values/{key}", value) + + def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigValueType) -> None: + """ + Set (raw) single config(entry) value for a player. + + Note that this only stores the (raw) value without any validation or default. + """ + if not self.get(f"{CONF_PLAYERS}/{player_id}"): + # only allow setting raw values if main entry exists + msg = f"Invalid player_id: {player_id}" + raise KeyError(msg) + if key in BASE_KEYS: + self.set(f"{CONF_PLAYERS}/{player_id}/{key}", value) + else: + self.set(f"{CONF_PLAYERS}/{player_id}/values/{key}", value) + + def save(self, immediate: bool = False) -> None: + """Schedule save of data to disk.""" + self._value_cache = {} + if self._timer_handle is not None: + self._timer_handle.cancel() + self._timer_handle = None + + if immediate: + self.mass.loop.create_task(self._async_save()) + else: + # schedule the save for later + self._timer_handle = self.mass.loop.call_later( + DEFAULT_SAVE_DELAY, self.mass.create_task, self._async_save + ) + + def encrypt_string(self, str_value: str) -> str: + """Encrypt a (password)string with Fernet.""" + if str_value.startswith(ENCRYPT_SUFFIX): + return str_value + return ENCRYPT_SUFFIX + self._fernet.encrypt(str_value.encode()).decode() + + def decrypt_string(self, encrypted_str: str) -> str: + """Decrypt a (password)string with Fernet.""" + if not encrypted_str: + return encrypted_str + if not encrypted_str.startswith(ENCRYPT_SUFFIX): + return encrypted_str + try: + return self._fernet.decrypt(encrypted_str.replace(ENCRYPT_SUFFIX, "").encode()).decode() + except InvalidToken as err: + msg = "Password decryption failed" + raise InvalidDataError(msg) from err + + async def _load(self) -> None: + """Load data from persistent storage.""" + assert not self._data, "Already loaded" + + for filename in (self.filename, f"{self.filename}.backup"): + try: + async with aiofiles.open(filename, encoding="utf-8") as _file: + self._data = json_loads(await _file.read()) + LOGGER.debug("Loaded persistent settings from %s", filename) + await self._migrate() + return + except FileNotFoundError: + pass + except JSON_DECODE_EXCEPTIONS: + LOGGER.exception("Error while reading persistent storage file %s", filename) + LOGGER.debug("Started with empty storage: No persistent storage file found.") + + async def _migrate(self) -> None: + changed = False + + # Older versions of MA can create corrupt entries with no domain if retrying + # logic runs after a provider has been removed. Remove those corrupt entries. + for instance_id, provider_config in list(self._data.get(CONF_PROVIDERS, {}).items()): + if "domain" not in provider_config: + self._data[CONF_PROVIDERS].pop(instance_id, None) + LOGGER.warning("Removed corrupt provider configuration: %s", instance_id) + changed = True + + if changed: + await self._async_save() + + async def _async_save(self) -> None: + """Save persistent data to disk.""" + filename_backup = f"{self.filename}.backup" + # make backup before we write a new file + if await isfile(self.filename): + if await isfile(filename_backup): + await remove(filename_backup) + await rename(self.filename, filename_backup) + + async with aiofiles.open(self.filename, "w", encoding="utf-8") as _file: + await _file.write(json_dumps(self._data, indent=True)) + LOGGER.debug("Saved data to persistent storage") + + @api_command("config/providers/reload") + async def _reload_provider(self, instance_id: str) -> None: + """Reload provider.""" + try: + config = await self.get_provider_config(instance_id) + except KeyError: + # Edge case: Provider was removed before we could reload it + return + await self.mass.load_provider_config(config) + + async def _update_provider_config( + self, instance_id: str, values: dict[str, ConfigValueType] + ) -> ProviderConfig: + """Update ProviderConfig.""" + config = await self.get_provider_config(instance_id) + changed_keys = config.update(values) + available = prov.available if (prov := self.mass.get_provider(instance_id)) else False + if not changed_keys and (config.enabled == available): + # no changes + return config + # validate the new config + config.validate() + # save the config first to prevent issues when the + # provider wants to manipulate the config during load + conf_key = f"{CONF_PROVIDERS}/{config.instance_id}" + raw_conf = config.to_raw() + self.set(conf_key, raw_conf) + if config.enabled: + await self.mass.load_provider_config(config) + else: + # disable provider + prov_manifest = self.mass.get_provider_manifest(config.domain) + if not prov_manifest.allow_disable: + msg = "Provider can not be disabled." + raise RuntimeError(msg) + # also unload any other providers dependent of this provider + for dep_prov in self.mass.providers: + if dep_prov.manifest.depends_on == config.domain: + await self.mass.unload_provider(dep_prov.instance_id) + await self.mass.unload_provider(config.instance_id) + if config.type == ProviderType.PLAYER: + # cleanup entries in player manager + for player in self.mass.players.all(return_unavailable=True, return_disabled=True): + if player.provider != instance_id: + continue + self.mass.players.remove(player.player_id, cleanup_config=False) + return config + + async def _add_provider_config( + self, + provider_domain: str, + values: dict[str, ConfigValueType], + ) -> list[ConfigEntry] | ProviderConfig: + """ + Add new Provider (instance). + + params: + - provider_domain: domain of the provider for which to add an instance of. + - values: the raw values for config entries. + + Returns: newly created ProviderConfig. + """ + # lookup provider manifest and module + for prov in self.mass.get_provider_manifests(): + if prov.domain == provider_domain: + manifest = prov + break + else: + msg = f"Unknown provider domain: {provider_domain}" + raise KeyError(msg) + if prov.depends_on and not self.mass.get_provider(prov.depends_on): + msg = f"Provider {manifest.name} depends on {prov.depends_on}" + raise ValueError(msg) + # create new provider config with given values + existing = { + x.instance_id for x in await self.get_provider_configs(provider_domain=provider_domain) + } + # determine instance id based on previous configs + if existing and not manifest.multi_instance: + msg = f"Provider {manifest.name} does not support multiple instances" + raise ValueError(msg) + 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 + ) + config: ProviderConfig = ProviderConfig.parse( + config_entries, + { + "type": manifest.type.value, + "domain": manifest.domain, + "instance_id": instance_id, + "name": manifest.name, + "values": values, + }, + ) + # validate the new config + config.validate() + # save the config first to prevent issues when the + # provider wants to manipulate the config during load + conf_key = f"{CONF_PROVIDERS}/{config.instance_id}" + self.set(conf_key, config.to_raw()) + # try to load the provider + try: + await self.mass.load_provider_config(config) + except Exception: + # loading failed, remove config + self.remove(conf_key) + raise + return config diff --git a/music_assistant/controllers/media/__init__.py b/music_assistant/controllers/media/__init__.py new file mode 100644 index 00000000..3256b9e0 --- /dev/null +++ b/music_assistant/controllers/media/__init__.py @@ -0,0 +1 @@ +"""Package with Media controllers.""" diff --git a/music_assistant/controllers/media/albums.py b/music_assistant/controllers/media/albums.py new file mode 100644 index 00000000..b0cfb856 --- /dev/null +++ b/music_assistant/controllers/media/albums.py @@ -0,0 +1,520 @@ +"""Manage MediaItems of type Album.""" + +from __future__ import annotations + +import contextlib +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any + +from music_assistant_models.enums import CacheCategory, ProviderFeature +from music_assistant_models.errors import ( + InvalidDataError, + MediaNotFoundError, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + ItemMapping, + MediaType, + Track, + UniqueList, +) + +from music_assistant.constants import DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS +from music_assistant.controllers.media.base import MediaControllerBase +from music_assistant.helpers.compare import ( + compare_album, + compare_artists, + compare_media_item, + loose_compare_strings, +) +from music_assistant.helpers.json import serialize_to_json + +if TYPE_CHECKING: + from music_assistant.models.music_provider import MusicProvider + + +class AlbumsController(MediaControllerBase[Album]): + """Controller managing MediaItems of type Album.""" + + db_table = DB_TABLE_ALBUMS + media_type = MediaType.ALBUM + item_cls = Album + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + self.base_query = """ + SELECT + albums.*, + (SELECT JSON_GROUP_ARRAY( + json_object( + 'item_id', provider_mappings.provider_item_id, + 'provider_domain', provider_mappings.provider_domain, + 'provider_instance', provider_mappings.provider_instance, + 'available', provider_mappings.available, + 'audio_format', json(provider_mappings.audio_format), + 'url', provider_mappings.url, + 'details', provider_mappings.details + )) FROM provider_mappings WHERE provider_mappings.item_id = albums.item_id AND media_type = 'album') AS provider_mappings, + (SELECT JSON_GROUP_ARRAY( + json_object( + 'item_id', artists.item_id, + 'provider', 'library', + 'name', artists.name, + 'sort_name', artists.sort_name, + 'media_type', 'artist' + )) FROM artists JOIN album_artists on album_artists.album_id = albums.item_id WHERE artists.item_id = album_artists.artist_id) AS artists + FROM albums""" # noqa: E501 + # register (extra) api handlers + api_base = self.api_base + self.mass.register_api_command(f"music/{api_base}/album_tracks", self.tracks) + self.mass.register_api_command(f"music/{api_base}/album_versions", self.versions) + + async def get( + self, + item_id: str, + provider_instance_id_or_domain: str, + recursive: bool = True, + ) -> Album: + """Return (full) details for a single media item.""" + album = await super().get( + item_id, + provider_instance_id_or_domain, + ) + if not recursive: + return album + + # append artist details to full album item (resolve ItemMappings) + album_artists = UniqueList() + for artist in album.artists: + if not isinstance(artist, ItemMapping): + album_artists.append(artist) + continue + with contextlib.suppress(MediaNotFoundError): + album_artists.append( + await self.mass.music.artists.get( + artist.item_id, + artist.provider, + ) + ) + album.artists = album_artists + return album + + async def library_items( + self, + favorite: bool | None = None, + search: str | None = None, + limit: int = 500, + offset: int = 0, + order_by: str = "sort_name", + provider: str | None = None, + extra_query: str | None = None, + extra_query_params: dict[str, Any] | None = None, + album_types: list[AlbumType] | None = None, + ) -> list[Artist]: + """Get in-database albums.""" + extra_query_params: dict[str, Any] = extra_query_params or {} + extra_query_parts: list[str] = [extra_query] if extra_query else [] + extra_join_parts: list[str] = [] + artist_table_joined = False + # optional album type filter + if album_types: + extra_query_parts.append("albums.album_type IN :album_types") + extra_query_params["album_types"] = [x.value for x in album_types] + if order_by and "artist_name" in order_by: + # join artist table to allow sorting on artist name + extra_join_parts.append( + "JOIN album_artists ON album_artists.album_id = albums.item_id " + "JOIN artists ON artists.item_id = album_artists.artist_id " + ) + artist_table_joined = True + if search and " - " in search: + # handle combined artist + title search + artist_str, title_str = search.split(" - ", 1) + search = None + extra_query_parts.append("albums.name LIKE :search_title") + extra_query_params["search_title"] = f"%{title_str}%" + # use join with artists table to filter on artist name + extra_join_parts.append( + "JOIN album_artists ON album_artists.album_id = albums.item_id " + "JOIN artists ON artists.item_id = album_artists.artist_id " + "AND artists.name LIKE :search_artist" + if not artist_table_joined + else "AND artists.name LIKE :search_artist" + ) + artist_table_joined = True + extra_query_params["search_artist"] = f"%{artist_str}%" + result = await self._get_library_items_by_query( + favorite=favorite, + search=search, + limit=limit, + offset=offset, + order_by=order_by, + provider=provider, + extra_query_parts=extra_query_parts, + extra_query_params=extra_query_params, + extra_join_parts=extra_join_parts, + ) + if search and len(result) < 25 and not offset: + # append artist items to result + extra_join_parts.append( + "JOIN album_artists ON album_artists.album_id = albums.item_id " + "JOIN artists ON artists.item_id = album_artists.artist_id " + "AND artists.name LIKE :search_artist" + if not artist_table_joined + else "AND artists.name LIKE :search_artist" + ) + extra_query_params["search_artist"] = f"%{search}%" + return result + await self._get_library_items_by_query( + favorite=favorite, + search=None, + limit=limit, + order_by=order_by, + provider=provider, + extra_query_parts=extra_query_parts, + extra_query_params=extra_query_params, + extra_join_parts=extra_join_parts, + ) + return result + + async def library_count( + self, favorite_only: bool = False, album_types: list[AlbumType] | None = None + ) -> int: + """Return the total number of items in the library.""" + sql_query = f"SELECT item_id FROM {self.db_table}" + query_parts: list[str] = [] + query_params: dict[str, Any] = {} + if favorite_only: + query_parts.append("favorite = 1") + if album_types: + query_parts.append("albums.album_type IN :album_types") + query_params["album_types"] = [x.value for x in album_types] + if query_parts: + sql_query += f" WHERE {' AND '.join(query_parts)}" + return await self.mass.music.database.get_count_from_query(sql_query, query_params) + + async def remove_item_from_library(self, item_id: str | int) -> None: + """Delete record from the database.""" + db_id = int(item_id) # ensure integer + # recursively also remove album tracks + for db_track in await self.get_library_album_tracks(db_id): + with contextlib.suppress(MediaNotFoundError): + await self.mass.music.tracks.remove_item_from_library(db_track.item_id) + # delete entry(s) from albumtracks table + await self.mass.music.database.delete(DB_TABLE_ALBUM_TRACKS, {"album_id": db_id}) + # delete entry(s) from album artists table + await self.mass.music.database.delete(DB_TABLE_ALBUM_ARTISTS, {"album_id": db_id}) + # delete the album itself from db + await super().remove_item_from_library(item_id) + + async def tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + in_library_only: bool = False, + ) -> UniqueList[Track]: + """Return album tracks for the given provider album id.""" + # always check if we have a library item for this album + library_album = await self.get_library_item_by_prov_id( + item_id, provider_instance_id_or_domain + ) + if not library_album: + return await self._get_provider_album_tracks(item_id, provider_instance_id_or_domain) + db_items = await self.get_library_album_tracks(library_album.item_id) + result: UniqueList[Track] = UniqueList(db_items) + if in_library_only: + # return in-library items only + return sorted(db_items, key=lambda x: (x.disc_number, x.track_number)) + + # return all (unique) items from all providers + # because we are returning the items from all providers combined, + # we need to make sure that we don't return duplicates + unique_ids: set[str] = {f"{x.disc_number}.{x.track_number}" for x in db_items} + unique_ids.update({f"{x.name.lower()}.{x.version.lower()}" for x in db_items}) + for db_item in db_items: + unique_ids.add(x.item_id for x in db_item.provider_mappings) + for provider_mapping in library_album.provider_mappings: + provider_tracks = await self._get_provider_album_tracks( + provider_mapping.item_id, provider_mapping.provider_instance + ) + for provider_track in provider_tracks: + if provider_track.item_id in unique_ids: + continue + unique_id = f"{provider_track.disc_number}.{provider_track.track_number}" + if unique_id in unique_ids: + continue + unique_id = f"{provider_track.name.lower()}.{provider_track.version.lower()}" + if unique_id in unique_ids: + continue + unique_ids.add(unique_id) + provider_track.album = library_album + result.append(provider_track) + # NOTE: we need to return the results sorted on disc/track here + # to ensure the correct order at playback + return sorted(result, key=lambda x: (x.disc_number, x.track_number)) + + async def versions( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> UniqueList[Album]: + """Return all versions of an album we can find on all providers.""" + album = await self.get_provider_item(item_id, provider_instance_id_or_domain) + search_query = f"{album.artists[0].name} - {album.name}" if album.artists else album.name + result: UniqueList[Album] = UniqueList() + for provider_id in self.mass.music.get_unique_providers(): + provider = self.mass.get_provider(provider_id) + if not provider: + continue + if not provider.library_supported(MediaType.ALBUM): + continue + result.extend( + prov_item + for prov_item in await self.search(search_query, provider_id) + if loose_compare_strings(album.name, prov_item.name) + and compare_artists(prov_item.artists, album.artists, any_match=True) + # make sure that the 'base' version is NOT included + and not album.provider_mappings.intersection(prov_item.provider_mappings) + ) + return result + + async def get_library_album_tracks( + self, + item_id: str | int, + ) -> list[Track]: + """Return in-database album tracks for the given database album.""" + return await self.mass.music.tracks._get_library_items_by_query( + extra_query_parts=[f"WHERE album_tracks.album_id = {item_id}"], + ) + + async def _add_library_item(self, item: Album) -> int: + """Add a new record to the database.""" + if not isinstance(item, Album): + msg = "Not a valid Album object (ItemMapping can not be added to db)" + raise InvalidDataError(msg) + if not item.artists: + msg = "Album is missing artist(s)" + raise InvalidDataError(msg) + db_id = await self.mass.music.database.insert( + self.db_table, + { + "name": item.name, + "sort_name": item.sort_name, + "version": item.version, + "favorite": item.favorite, + "album_type": item.album_type, + "year": item.year, + "metadata": serialize_to_json(item.metadata), + "external_ids": serialize_to_json(item.external_ids), + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, item.provider_mappings) + # set track artist(s) + await self._set_album_artists(db_id, item.artists) + self.logger.debug("added %s to database (id: %s)", item.name, db_id) + return db_id + + async def _update_library_item( + self, item_id: str | int, update: Album, overwrite: bool = False + ) -> None: + """Update existing record in the database.""" + db_id = int(item_id) # ensure integer + cur_item = await self.get_library_item(db_id) + metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) + if getattr(update, "album_type", AlbumType.UNKNOWN) != AlbumType.UNKNOWN: + album_type = update.album_type + else: + album_type = cur_item.album_type + cur_item.external_ids.update(update.external_ids) + provider_mappings = ( + update.provider_mappings + if overwrite + else {*cur_item.provider_mappings, *update.provider_mappings} + ) + await self.mass.music.database.update( + self.db_table, + {"item_id": db_id}, + { + "name": update.name if overwrite else cur_item.name, + "sort_name": update.sort_name + if overwrite + else cur_item.sort_name or update.sort_name, + "version": update.version if overwrite else cur_item.version or update.version, + "year": update.year if overwrite else cur_item.year or update.year, + "album_type": album_type.value, + "metadata": serialize_to_json(metadata), + "external_ids": serialize_to_json( + update.external_ids if overwrite else cur_item.external_ids + ), + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, provider_mappings, overwrite) + # set album artist(s) + artists = update.artists if overwrite else cur_item.artists + update.artists + await self._set_album_artists(db_id, artists, overwrite=overwrite) + self.logger.debug("updated %s in database: (id %s)", update.name, db_id) + + async def _get_provider_album_tracks( + self, item_id: str, provider_instance_id_or_domain: str + ) -> list[Track]: + """Return album tracks for the given provider album id.""" + prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain) + if prov is None: + return [] + # prefer cache items (if any) - for streaming providers only + cache_category = CacheCategory.MUSIC_ALBUM_TRACKS + cache_base_key = prov.lookup_key + cache_key = item_id + if ( + prov.is_streaming_provider + and ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ) + is not None + ): + return [Track.from_dict(x) for x in cache] + # no items in cache - get listing from provider + items = await prov.get_album_tracks(item_id) + # store (serializable items) in cache + if prov.is_streaming_provider: + self.mass.create_task( + self.mass.cache.set(cache_key, [x.to_dict() for x in items]), + category=cache_category, + base_key=cache_base_key, + ) + for item in items: + # if this is a complete track object, pre-cache it as + # that will save us an (expensive) lookup later + if item.image and item.artist_str and item.album and prov.domain != "builtin": + await self.mass.cache.set( + f"track.{item_id}", + item.to_dict(), + category=CacheCategory.MUSIC_PROVIDER_ITEM, + base_key=prov.lookup_key, + ) + return items + + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ): + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + assert provider_instance_id_or_domain != "library" + return await self._get_provider_album_tracks(item_id, provider_instance_id_or_domain) + + async def _get_dynamic_tracks( + self, + media_item: Album, + limit: int = 25, + ) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) + msg = "No Music Provider found that supports requesting similar tracks." + raise UnsupportedFeaturedException(msg) + + async def _set_album_artists( + self, db_id: int, artists: Iterable[Artist | ItemMapping], overwrite: bool = False + ) -> None: + """Store Album Artists.""" + if overwrite: + # on overwrite, clear the album_artists table first + await self.mass.music.database.delete( + DB_TABLE_ALBUM_ARTISTS, + { + "album_id": db_id, + }, + ) + for artist in artists: + await self._set_album_artist(db_id, artist=artist, overwrite=overwrite) + + async def _set_album_artist( + self, db_id: int, artist: Artist | ItemMapping, overwrite: bool = False + ) -> ItemMapping: + """Store Album Artist info.""" + db_artist: Artist | ItemMapping = None + if artist.provider == "library": + db_artist = artist + elif existing := await self.mass.music.artists.get_library_item_by_prov_id( + artist.item_id, artist.provider + ): + db_artist = existing + + if not db_artist or overwrite: + db_artist = await self.mass.music.artists.add_item_to_library( + artist, overwrite_existing=overwrite + ) + # write (or update) record in album_artists table + await self.mass.music.database.insert_or_replace( + DB_TABLE_ALBUM_ARTISTS, + { + "album_id": db_id, + "artist_id": int(db_artist.item_id), + }, + ) + return ItemMapping.from_item(db_artist) + + async def match_providers(self, db_album: Album) -> None: + """Try to find match on all (streaming) providers for the provided (database) album. + + This is used to link objects of different providers/qualities together. + """ + if db_album.provider != "library": + return # Matching only supported for database items + if not db_album.artists: + return # guard + artist_name = db_album.artists[0].name + + async def find_prov_match(provider: MusicProvider): + self.logger.debug( + "Trying to match album %s on provider %s", db_album.name, provider.name + ) + match_found = False + search_str = f"{artist_name} - {db_album.name}" + search_result = await self.search(search_str, provider.instance_id) + for search_result_item in search_result: + if not search_result_item.available: + continue + if not compare_media_item(db_album, search_result_item): + continue + # we must fetch the full album version, search results can be simplified objects + prov_album = await self.get_provider_item( + search_result_item.item_id, + search_result_item.provider, + fallback=search_result_item, + ) + if compare_album(db_album, prov_album): + # 100% match, we update the db with the additional provider mapping(s) + match_found = True + for provider_mapping in search_result_item.provider_mappings: + await self.add_provider_mapping(db_album.item_id, provider_mapping) + db_album.provider_mappings.add(provider_mapping) + return match_found + + # try to find match on all providers + cur_provider_domains = {x.provider_domain for x in db_album.provider_mappings} + for provider in self.mass.music.providers: + if provider.domain in cur_provider_domains: + continue + if ProviderFeature.SEARCH not in provider.supported_features: + continue + if not provider.library_supported(MediaType.ALBUM): + continue + if not provider.is_streaming_provider: + # matching on unique providers is pointless as they push (all) their content to MA + continue + if await find_prov_match(provider): + cur_provider_domains.add(provider.domain) + else: + self.logger.debug( + "Could not find match for Album %s on provider %s", + db_album.name, + provider.name, + ) diff --git a/music_assistant/controllers/media/artists.py b/music_assistant/controllers/media/artists.py new file mode 100644 index 00000000..776d907a --- /dev/null +++ b/music_assistant/controllers/media/artists.py @@ -0,0 +1,545 @@ +"""Manage MediaItems of type Artist.""" + +from __future__ import annotations + +import asyncio +import contextlib +from typing import TYPE_CHECKING, Any + +from music_assistant_models.enums import CacheCategory, ProviderFeature +from music_assistant_models.errors import ( + MediaNotFoundError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + ItemMapping, + MediaType, + Track, + UniqueList, +) + +from music_assistant.constants import ( + DB_TABLE_ALBUM_ARTISTS, + DB_TABLE_ARTISTS, + DB_TABLE_TRACK_ARTISTS, + VARIOUS_ARTISTS_MBID, + VARIOUS_ARTISTS_NAME, +) +from music_assistant.controllers.media.base import MediaControllerBase +from music_assistant.helpers.compare import compare_artist, compare_strings +from music_assistant.helpers.json import serialize_to_json + +if TYPE_CHECKING: + from music_assistant.models.music_provider import MusicProvider + + +class ArtistsController(MediaControllerBase[Artist]): + """Controller managing MediaItems of type Artist.""" + + db_table = DB_TABLE_ARTISTS + media_type = MediaType.ARTIST + item_cls = Artist + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + self._db_add_lock = asyncio.Lock() + # register (extra) api handlers + api_base = self.api_base + self.mass.register_api_command(f"music/{api_base}/artist_albums", self.albums) + self.mass.register_api_command(f"music/{api_base}/artist_tracks", self.tracks) + + async def library_count( + self, favorite_only: bool = False, album_artists_only: bool = False + ) -> int: + """Return the total number of items in the library.""" + sql_query = f"SELECT item_id FROM {self.db_table}" + query_parts: list[str] = [] + if favorite_only: + query_parts.append("favorite = 1") + if album_artists_only: + query_parts.append( + f"item_id in (select {DB_TABLE_ALBUM_ARTISTS}.artist_id " + f"FROM {DB_TABLE_ALBUM_ARTISTS})" + ) + if query_parts: + sql_query += f" WHERE {' AND '.join(query_parts)}" + return await self.mass.music.database.get_count_from_query(sql_query) + + async def library_items( + self, + favorite: bool | None = None, + search: str | None = None, + limit: int = 500, + offset: int = 0, + order_by: str = "sort_name", + provider: str | None = None, + extra_query: str | None = None, + extra_query_params: dict[str, Any] | None = None, + album_artists_only: bool = False, + ) -> list[Artist]: + """Get in-database (album) artists.""" + extra_query_params: dict[str, Any] = extra_query_params or {} + extra_query_parts: list[str] = [extra_query] if extra_query else [] + if album_artists_only: + extra_query_parts.append( + f"artists.item_id in (select {DB_TABLE_ALBUM_ARTISTS}.artist_id " + f"from {DB_TABLE_ALBUM_ARTISTS})" + ) + return await self._get_library_items_by_query( + favorite=favorite, + search=search, + limit=limit, + offset=offset, + order_by=order_by, + provider=provider, + extra_query_parts=extra_query_parts, + extra_query_params=extra_query_params, + ) + + async def tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + in_library_only: bool = False, + ) -> UniqueList[Track]: + """Return all/top tracks for an artist.""" + # always check if we have a library item for this artist + library_artist = await self.get_library_item_by_prov_id( + item_id, provider_instance_id_or_domain + ) + if not library_artist: + return await self.get_provider_artist_toptracks(item_id, provider_instance_id_or_domain) + db_items = await self.get_library_artist_tracks(library_artist.item_id) + result: UniqueList[Track] = UniqueList(db_items) + if in_library_only: + # return in-library items only + return result + # return all (unique) items from all providers + unique_ids: set[str] = set() + for provider_mapping in library_artist.provider_mappings: + provider_tracks = await self.get_provider_artist_toptracks( + provider_mapping.item_id, provider_mapping.provider_instance + ) + for provider_track in provider_tracks: + unique_id = f"{provider_track.name}.{provider_track.version}" + if unique_id in unique_ids: + continue + unique_ids.add(unique_id) + # prefer db item + if db_item := await self.mass.music.tracks.get_library_item_by_prov_id( + provider_track.item_id, provider_track.provider + ): + result.append(db_item) + elif not in_library_only: + result.append(provider_track) + return result + + async def albums( + self, + item_id: str, + provider_instance_id_or_domain: str, + in_library_only: bool = False, + ) -> UniqueList[Album]: + """Return (all/most popular) albums for an artist.""" + # always check if we have a library item for this artist + library_artist = await self.get_library_item_by_prov_id( + item_id, provider_instance_id_or_domain + ) + if not library_artist: + return await self.get_provider_artist_albums(item_id, provider_instance_id_or_domain) + db_items = await self.get_library_artist_albums(library_artist.item_id) + result: UniqueList[Album] = UniqueList(db_items) + if in_library_only: + # return in-library items only + return result + # return all (unique) items from all providers + unique_ids: set[str] = set() + for provider_mapping in library_artist.provider_mappings: + provider_albums = await self.get_provider_artist_albums( + provider_mapping.item_id, provider_mapping.provider_instance + ) + for provider_album in provider_albums: + unique_id = f"{provider_album.name}.{provider_album.version}" + if unique_id in unique_ids: + continue + unique_ids.add(unique_id) + # prefer db item + if db_item := await self.mass.music.albums.get_library_item_by_prov_id( + provider_album.item_id, provider_album.provider + ): + result.append(db_item) + elif not in_library_only: + result.append(provider_album) + return result + + async def remove_item_from_library(self, item_id: str | int) -> None: + """Delete record from the database.""" + db_id = int(item_id) # ensure integer + # recursively also remove artist albums + for db_row in await self.mass.music.database.get_rows_from_query( + f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {db_id}", + limit=5000, + ): + with contextlib.suppress(MediaNotFoundError): + await self.mass.music.albums.remove_item_from_library(db_row["album_id"]) + + # recursively also remove artist tracks + for db_row in await self.mass.music.database.get_rows_from_query( + f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {db_id}", + limit=5000, + ): + with contextlib.suppress(MediaNotFoundError): + await self.mass.music.tracks.remove_item_from_library(db_row["track_id"]) + + # delete the artist itself from db + await super().remove_item_from_library(db_id) + + async def get_provider_artist_toptracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> list[Track]: + """Return top tracks for an artist on given provider.""" + items = [] + assert provider_instance_id_or_domain != "library" + prov = self.mass.get_provider(provider_instance_id_or_domain) + if prov is None: + return [] + # prefer cache items (if any) - for streaming providers + cache_category = CacheCategory.MUSIC_ARTIST_TRACKS + cache_base_key = prov.lookup_key + cache_key = item_id + if ( + prov.is_streaming_provider + and ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ) + is not None + ): + return [Track.from_dict(x) for x in cache] + # no items in cache - get listing from provider + if ProviderFeature.ARTIST_TOPTRACKS in prov.supported_features: + items = await prov.get_artist_toptracks(item_id) + for item in items: + # if this is a complete track object, pre-cache it as + # that will save us an (expensive) lookup later + if item.image and item.artist_str and item.album and prov.domain != "builtin": + await self.mass.cache.set( + f"track.{item_id}", + item.to_dict(), + category=CacheCategory.MUSIC_PROVIDER_ITEM, + base_key=prov.lookup_key, + ) + else: + # fallback implementation using the db + if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( + item_id, + provider_instance_id_or_domain, + ): + artist_id = db_artist.item_id + subquery = ( + f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {artist_id}" + ) + query = f"tracks.item_id in ({subquery})" + return await self.mass.music.tracks._get_library_items_by_query( + extra_query_parts=[query], provider=provider_instance_id_or_domain + ) + # store (serializable items) in cache + if prov.is_streaming_provider: + self.mass.create_task( + self.mass.cache.set( + cache_key, + [x.to_dict() for x in items], + category=cache_category, + base_key=cache_base_key, + ) + ) + return items + + async def get_library_artist_tracks( + self, + item_id: str | int, + ) -> list[Track]: + """Return all tracks for an artist in the library/db.""" + subquery = f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {item_id}" + query = f"tracks.item_id in ({subquery})" + return await self.mass.music.tracks._get_library_items_by_query(extra_query_parts=[query]) + + async def get_provider_artist_albums( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> list[Album]: + """Return albums for an artist on given provider.""" + items = [] + assert provider_instance_id_or_domain != "library" + prov = self.mass.get_provider(provider_instance_id_or_domain) + if prov is None: + return [] + # prefer cache items (if any) + cache_category = CacheCategory.MUSIC_ARTIST_ALBUMS + cache_base_key = prov.lookup_key + cache_key = item_id + if ( + prov.is_streaming_provider + and ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ) + is not None + ): + return [Album.from_dict(x) for x in cache] + # no items in cache - get listing from provider + if ProviderFeature.ARTIST_ALBUMS in prov.supported_features: + items = await prov.get_artist_albums(item_id) + else: + # fallback implementation using the db + # ruff: noqa: PLR5501 + if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( + item_id, + provider_instance_id_or_domain, + ): + artist_id = db_artist.item_id + subquery = ( + f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {artist_id}" + ) + query = f"albums.item_id in ({subquery})" + return await self.mass.music.albums._get_library_items_by_query( + extra_query_parts=[query], provider=provider_instance_id_or_domain + ) + + # store (serializable items) in cache + if prov.is_streaming_provider: + self.mass.create_task( + self.mass.cache.set( + cache_key, + [x.to_dict() for x in items], + category=cache_category, + base_key=cache_base_key, + ) + ) + return items + + async def get_library_artist_albums( + self, + item_id: str | int, + ) -> list[Album]: + """Return all in-library albums for an artist.""" + subquery = f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {item_id}" + query = f"albums.item_id in ({subquery})" + return await self.mass.music.albums._get_library_items_by_query(extra_query_parts=[query]) + + async def _add_library_item(self, item: Artist | ItemMapping) -> int: + """Add a new item record to the database.""" + if isinstance(item, ItemMapping): + item = self._artist_from_item_mapping(item) + # enforce various artists name + id + if compare_strings(item.name, VARIOUS_ARTISTS_NAME): + item.mbid = VARIOUS_ARTISTS_MBID + if item.mbid == VARIOUS_ARTISTS_MBID: + item.name = VARIOUS_ARTISTS_NAME + # no existing item matched: insert item + db_id = await self.mass.music.database.insert( + self.db_table, + { + "name": item.name, + "sort_name": item.sort_name, + "favorite": item.favorite, + "external_ids": serialize_to_json(item.external_ids), + "metadata": serialize_to_json(item.metadata), + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, item.provider_mappings) + self.logger.debug("added %s to database (id: %s)", item.name, db_id) + return db_id + + async def _update_library_item( + self, item_id: str | int, update: Artist | ItemMapping, overwrite: bool = False + ) -> None: + """Update existing record in the database.""" + db_id = int(item_id) # ensure integer + cur_item = await self.get_library_item(db_id) + if isinstance(update, ItemMapping): + # NOTE that artist is the only mediatype where its accepted we + # receive an itemmapping from streaming providers + update = self._artist_from_item_mapping(update) + metadata = cur_item.metadata + else: + metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) + cur_item.external_ids.update(update.external_ids) + # enforce various artists name + id + mbid = cur_item.mbid + if (not mbid or overwrite) and getattr(update, "mbid", None): + if compare_strings(update.name, VARIOUS_ARTISTS_NAME): + update.mbid = VARIOUS_ARTISTS_MBID + if update.mbid == VARIOUS_ARTISTS_MBID: + update.name = VARIOUS_ARTISTS_NAME + + await self.mass.music.database.update( + self.db_table, + {"item_id": db_id}, + { + "name": update.name if overwrite else cur_item.name, + "sort_name": update.sort_name + if overwrite + else cur_item.sort_name or update.sort_name, + "external_ids": serialize_to_json( + update.external_ids if overwrite else cur_item.external_ids + ), + "metadata": serialize_to_json(metadata), + }, + ) + self.logger.debug("updated %s in database: %s", update.name, db_id) + # update/set provider_mappings table + provider_mappings = ( + update.provider_mappings + if overwrite + else {*cur_item.provider_mappings, *update.provider_mappings} + ) + await self._set_provider_mappings(db_id, provider_mappings, overwrite) + self.logger.debug("updated %s in database: (id %s)", update.name, db_id) + + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ): + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + assert provider_instance_id_or_domain != "library" + return await self.get_provider_artist_toptracks( + item_id, + provider_instance_id_or_domain, + ) + + async def _get_dynamic_tracks( + self, + media_item: Artist, + limit: int = 25, + ) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) + msg = "No Music Provider found that supports requesting similar tracks." + raise UnsupportedFeaturedException(msg) + + async def match_providers(self, db_artist: Artist) -> None: + """Try to find matching artists on all providers for the provided (database) item_id. + + This is used to link objects of different providers together. + """ + assert db_artist.provider == "library", "Matching only supported for database items!" + cur_provider_domains = {x.provider_domain for x in db_artist.provider_mappings} + for provider in self.mass.music.providers: + if provider.domain in cur_provider_domains: + continue + if ProviderFeature.SEARCH not in provider.supported_features: + continue + if not provider.library_supported(MediaType.ARTIST): + continue + if not provider.is_streaming_provider: + # matching on unique providers is pointless as they push (all) their content to MA + continue + if await self._match_provider(db_artist, provider): + cur_provider_domains.add(provider.domain) + else: + self.logger.debug( + "Could not find match for Artist %s on provider %s", + db_artist.name, + provider.name, + ) + + async def _match_provider(self, db_artist: Artist, provider: MusicProvider) -> bool: + """Try to find matching artists on given provider for the provided (database) artist.""" + self.logger.debug("Trying to match artist %s on provider %s", db_artist.name, provider.name) + # try to get a match with some reference tracks of this artist + ref_tracks = await self.mass.music.artists.tracks(db_artist.item_id, db_artist.provider) + if len(ref_tracks) < 10: + # fetch reference tracks from provider(s) attached to the artist + for provider_mapping in db_artist.provider_mappings: + with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError): + ref_tracks += await self.mass.music.artists.tracks( + provider_mapping.item_id, provider_mapping.provider_instance + ) + for ref_track in ref_tracks: + search_str = f"{db_artist.name} - {ref_track.name}" + search_results = await self.mass.music.tracks.search(search_str, provider.domain) + for search_result_item in search_results: + if not compare_strings(search_result_item.name, ref_track.name, strict=True): + continue + # get matching artist from track + for search_item_artist in search_result_item.artists: + if not compare_strings(search_item_artist.name, db_artist.name, strict=True): + continue + # 100% track match + # get full artist details so we have all metadata + prov_artist = await self.get_provider_item( + search_item_artist.item_id, + search_item_artist.provider, + fallback=search_result_item, + ) + # 100% match, we update the db with the additional provider mapping(s) + for provider_mapping in prov_artist.provider_mappings: + await self.add_provider_mapping(db_artist.item_id, provider_mapping) + db_artist.provider_mappings.add(provider_mapping) + return True + # try to get a match with some reference albums of this artist + ref_albums = await self.mass.music.artists.albums(db_artist.item_id, db_artist.provider) + if len(ref_albums) < 10: + # fetch reference albums from provider(s) attached to the artist + for provider_mapping in db_artist.provider_mappings: + with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError): + ref_albums += await self.mass.music.artists.albums( + provider_mapping.item_id, provider_mapping.provider_instance + ) + for ref_album in ref_albums: + if ref_album.album_type == AlbumType.COMPILATION: + continue + if not ref_album.artists: + continue + search_str = f"{db_artist.name} - {ref_album.name}" + search_result = await self.mass.music.albums.search(search_str, provider.domain) + for search_result_item in search_result: + if not search_result_item.artists: + continue + if not compare_strings(search_result_item.name, ref_album.name): + continue + # artist must match 100% + if not compare_artist(db_artist, search_result_item.artists[0]): + continue + # 100% match + # get full artist details so we have all metadata + prov_artist = await self.get_provider_item( + search_result_item.artists[0].item_id, + search_result_item.artists[0].provider, + fallback=search_result_item.artists[0], + ) + await self._update_library_item(db_artist.item_id, prov_artist) + return True + return False + + def _artist_from_item_mapping(self, item: ItemMapping) -> Artist: + domain, instance_id = None, None + if prov := self.mass.get_provider(item.provider): + domain = prov.domain + instance_id = prov.instance_id + return Artist.from_dict( + { + **item.to_dict(), + "provider_mappings": [ + { + "item_id": item.item_id, + "provider_domain": domain, + "provider_instance": instance_id, + "available": item.available, + } + ], + } + ) diff --git a/music_assistant/controllers/media/base.py b/music_assistant/controllers/media/base.py new file mode 100644 index 00000000..5fc7b8b0 --- /dev/null +++ b/music_assistant/controllers/media/base.py @@ -0,0 +1,819 @@ +"""Base (ABC) MediaType specific controller.""" + +from __future__ import annotations + +import asyncio +import logging +from abc import ABCMeta, abstractmethod +from collections.abc import Iterable +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from music_assistant_models.enums import ( + CacheCategory, + EventType, + ExternalID, + MediaType, + ProviderFeature, +) +from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError +from music_assistant_models.media_items import ( + Album, + ItemMapping, + MediaItemType, + ProviderMapping, + SearchResults, + Track, +) + +from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PROVIDER_MAPPINGS, MASS_LOGGER_NAME +from music_assistant.helpers.compare import compare_media_item +from music_assistant.helpers.json import json_loads, serialize_to_json + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Mapping + + from music_assistant import MusicAssistant + +ItemCls = TypeVar("ItemCls", bound="MediaItemType") + +JSON_KEYS = ("artists", "track_album", "metadata", "provider_mappings", "external_ids") + +SORT_KEYS = { + "name": "name COLLATE NOCASE ASC", + "name_desc": "name COLLATE NOCASE DESC", + "sort_name": "sort_name COLLATE NOCASE ASC", + "sort_name_desc": "sort_name COLLATE NOCASE DESC", + "timestamp_added": "timestamp_added ASC", + "timestamp_added_desc": "timestamp_added DESC", + "timestamp_modified": "timestamp_modified ASC", + "timestamp_modified_desc": "timestamp_modified DESC", + "last_played": "last_played ASC", + "last_played_desc": "last_played DESC", + "play_count": "play_count ASC", + "play_count_desc": "play_count DESC", + "year": "year ASC", + "year_desc": "year DESC", + "position": "position ASC", + "position_desc": "position DESC", + "artist_name": "artists.name COLLATE NOCASE ASC", + "artist_name_desc": "artists.name COLLATE NOCASE DESC", + "random": "RANDOM()", + "random_play_count": "RANDOM(), play_count ASC", +} + + +class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): + """Base model for controller managing a MediaType.""" + + media_type: MediaType + item_cls: MediaItemType + db_table: str + + def __init__(self, mass: MusicAssistant) -> None: + """Initialize class.""" + self.mass = mass + self.base_query = f""" + SELECT + {self.db_table}.*, + (SELECT JSON_GROUP_ARRAY( + json_object( + 'item_id', provider_mappings.provider_item_id, + 'provider_domain', provider_mappings.provider_domain, + 'provider_instance', provider_mappings.provider_instance, + 'available', provider_mappings.available, + 'audio_format', json(provider_mappings.audio_format), + 'url', provider_mappings.url, + 'details', provider_mappings.details + )) FROM provider_mappings WHERE provider_mappings.item_id = {self.db_table}.item_id + AND provider_mappings.media_type = '{self.media_type.value}') AS provider_mappings + FROM {self.db_table} """ # noqa: E501 + self.logger = logging.getLogger(f"{MASS_LOGGER_NAME}.music.{self.media_type.value}") + # register (base) api handlers + self.api_base = api_base = f"{self.media_type}s" + self.mass.register_api_command(f"music/{api_base}/count", self.library_count) + self.mass.register_api_command(f"music/{api_base}/library_items", self.library_items) + self.mass.register_api_command(f"music/{api_base}/get", self.get) + self.mass.register_api_command(f"music/{api_base}/get_{self.media_type}", self.get) + self.mass.register_api_command(f"music/{api_base}/add", self.add_item_to_library) + self.mass.register_api_command(f"music/{api_base}/update", self.update_item_in_library) + self.mass.register_api_command(f"music/{api_base}/remove", self.remove_item_from_library) + self._db_add_lock = asyncio.Lock() + + async def add_item_to_library( + self, + item: ItemCls, + overwrite_existing: bool = False, + ) -> ItemCls: + """Add item to library and return the new (or updated) database item.""" + new_item = False + # check for existing item first + if library_id := await self._get_library_item_by_match(item): + # update existing item + await self._update_library_item(library_id, item, overwrite=overwrite_existing) + else: + # actually add a new item in the library db + async with self._db_add_lock: + library_id = await self._add_library_item(item) + new_item = True + # return final library_item + library_item = await self.get_library_item(library_id) + self.mass.signal_event( + EventType.MEDIA_ITEM_ADDED if new_item else EventType.MEDIA_ITEM_UPDATED, + library_item.uri, + library_item, + ) + return library_item + + async def _get_library_item_by_match(self, item: Track | ItemMapping) -> int | None: + if item.provider == "library": + return int(item.item_id) + # search by provider mappings + if isinstance(item, ItemMapping): + if cur_item := await self.get_library_item_by_prov_id(item.item_id, item.provider): + return cur_item.item_id + elif cur_item := await self.get_library_item_by_prov_mappings(item.provider_mappings): + return cur_item.item_id + if cur_item := await self.get_library_item_by_external_ids(item.external_ids): + # existing item match by external id + # Double check external IDs - if MBID exists, regards that as overriding + if compare_media_item(item, cur_item): + return cur_item.item_id + # search by (exact) name match + query = f"{self.db_table}.name = :name OR {self.db_table}.sort_name = :sort_name" + query_params = {"name": item.name, "sort_name": item.sort_name} + async for db_item in self.iter_library_items( + extra_query=query, extra_query_params=query_params + ): + if compare_media_item(db_item, item, True): + return db_item.item_id + return None + + async def update_item_in_library( + self, item_id: str | int, update: ItemCls, overwrite: bool = False + ) -> ItemCls: + """Update existing library record in the library database.""" + await self._update_library_item(item_id, update, overwrite=overwrite) + # return the updated object + library_item = await self.get_library_item(item_id) + self.mass.signal_event( + EventType.MEDIA_ITEM_UPDATED, + library_item.uri, + library_item, + ) + return library_item + + async def remove_item_from_library(self, item_id: str | int) -> None: + """Delete library record from the database.""" + db_id = int(item_id) # ensure integer + library_item = await self.get_library_item(db_id) + assert library_item, f"Item does not exist: {db_id}" + # delete item + await self.mass.music.database.delete( + self.db_table, + {"item_id": db_id}, + ) + # update provider_mappings table + await self.mass.music.database.delete( + DB_TABLE_PROVIDER_MAPPINGS, + {"media_type": self.media_type.value, "item_id": db_id}, + ) + # cleanup playlog table + await self.mass.music.database.delete( + DB_TABLE_PLAYLOG, + { + "media_type": self.media_type.value, + "item_id": db_id, + "provider": "library", + }, + ) + for prov_mapping in library_item.provider_mappings: + await self.mass.music.database.delete( + DB_TABLE_PLAYLOG, + { + "media_type": self.media_type.value, + "item_id": prov_mapping.item_id, + "provider": prov_mapping.provider_instance, + }, + ) + # NOTE: this does not delete any references to this item in other records, + # this is handled/overridden in the mediatype specific controllers + self.mass.signal_event(EventType.MEDIA_ITEM_DELETED, library_item.uri, library_item) + self.logger.debug("deleted item with id %s from database", db_id) + + async def library_count(self, favorite_only: bool = False) -> int: + """Return the total number of items in the library.""" + if favorite_only: + sql_query = f"SELECT item_id FROM {self.db_table} WHERE favorite = 1" + return await self.mass.music.database.get_count_from_query(sql_query) + return await self.mass.music.database.get_count(self.db_table) + + async def library_items( + self, + favorite: bool | None = None, + search: str | None = None, + limit: int = 500, + offset: int = 0, + order_by: str = "sort_name", + provider: str | None = None, + extra_query: str | None = None, + extra_query_params: dict[str, Any] | None = None, + ) -> list[ItemCls]: + """Get in-database items.""" + return await self._get_library_items_by_query( + favorite=favorite, + search=search, + limit=limit, + offset=offset, + order_by=order_by, + provider=provider, + extra_query_parts=[extra_query] if extra_query else None, + extra_query_params=extra_query_params, + ) + + async def iter_library_items( + self, + favorite: bool | None = None, + search: str | None = None, + order_by: str = "sort_name", + provider: str | None = None, + extra_query: str | None = None, + extra_query_params: dict[str, Any] | None = None, + ) -> AsyncGenerator[ItemCls, None]: + """Iterate all in-database items.""" + limit: int = 500 + offset: int = 0 + while True: + next_items = await self.library_items( + favorite=favorite, + search=search, + limit=limit, + offset=offset, + order_by=order_by, + provider=provider, + extra_query=extra_query, + extra_query_params=extra_query_params, + ) + for item in next_items: + yield item + if len(next_items) < limit: + break + offset += limit + + async def get( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> ItemCls: + """Return (full) details for a single media item.""" + # always prefer the full library item if we have it + if library_item := await self.get_library_item_by_prov_id( + item_id, + provider_instance_id_or_domain, + ): + # schedule a refresh of the metadata on access of the item + # e.g. the item is being played or opened in the UI + self.mass.metadata.schedule_update_metadata(library_item.uri) + return library_item + # grab full details from the provider + return await self.get_provider_item( + item_id, + provider_instance_id_or_domain, + ) + + async def search( + self, + search_query: str, + provider_instance_id_or_domain: str, + limit: int = 25, + ) -> list[ItemCls]: + """Search database or provider with given query.""" + # create safe search string + search_query = search_query.replace("/", " ").replace("'", "") + if provider_instance_id_or_domain == "library": + return await self.library_items(search=search_query, limit=limit) + prov = self.mass.get_provider(provider_instance_id_or_domain) + if prov is None: + return [] + if ProviderFeature.SEARCH not in prov.supported_features: + return [] + if not prov.library_supported(self.media_type): + # assume library supported also means that this mediatype is supported + return [] + + # prefer cache items (if any) + cache_category = CacheCategory.MUSIC_SEARCH + cache_base_key = prov.lookup_key + cache_key = f"{search_query}.{limit}.{self.media_type.value}" + if ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ) is not None: + searchresult = SearchResults.from_dict(cache) + else: + # no items in cache - get listing from provider + searchresult = await prov.search( + search_query, + [self.media_type], + limit, + ) + if self.media_type == MediaType.ARTIST: + items = searchresult.artists + elif self.media_type == MediaType.ALBUM: + items = searchresult.albums + elif self.media_type == MediaType.TRACK: + items = searchresult.tracks + elif self.media_type == MediaType.PLAYLIST: + items = searchresult.playlists + else: + items = searchresult.radio + # store (serializable items) in cache + if prov.is_streaming_provider: # do not cache filesystem results + self.mass.create_task( + self.mass.cache.set( + cache_key, + searchresult.to_dict(), + expiration=86400 * 7, + category=cache_category, + base_key=cache_base_key, + ), + ) + return items + + async def get_provider_mapping(self, item: ItemCls) -> tuple[str, str]: + """Return (first) provider and item id.""" + if not getattr(item, "provider_mappings", None): + if item.provider == "library": + item = await self.get_library_item(item.item_id) + return (item.provider, item.item_id) + for prefer_unique in (True, False): + for prov_mapping in item.provider_mappings: + if not prov_mapping.available: + continue + if provider := self.mass.get_provider( + prov_mapping.provider_instance + if prefer_unique + else prov_mapping.provider_domain + ): + if prefer_unique and provider.is_streaming_provider: + continue + return (prov_mapping.provider_instance, prov_mapping.item_id) + # last resort: return just the first entry + for prov_mapping in item.provider_mappings: + return (prov_mapping.provider_domain, prov_mapping.item_id) + + return (None, None) + + async def get_library_item(self, item_id: int | str) -> ItemCls: + """Get single library item by id.""" + db_id = int(item_id) # ensure integer + extra_query = f"WHERE {self.db_table}.item_id = {item_id}" + async for db_item in self.iter_library_items(extra_query=extra_query): + return db_item + msg = f"{self.media_type.value} not found in library: {db_id}" + raise MediaNotFoundError(msg) + + async def get_library_item_by_prov_id( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> ItemCls | None: + """Get the library item for the given provider_instance.""" + assert item_id + assert provider_instance_id_or_domain + if provider_instance_id_or_domain == "library": + return await self.get_library_item(item_id) + for item in await self.get_library_items_by_prov_id( + provider_instance_id_or_domain=provider_instance_id_or_domain, + provider_item_id=item_id, + ): + return item + return None + + async def get_library_item_by_prov_mappings( + self, + provider_mappings: list[ProviderMapping], + ) -> ItemCls | None: + """Get the library item for the given provider_instance.""" + # always prefer provider instance first + for mapping in provider_mappings: + for item in await self.get_library_items_by_prov_id( + provider_instance=mapping.provider_instance, + provider_item_id=mapping.item_id, + ): + return item + # check by domain too + for mapping in provider_mappings: + for item in await self.get_library_items_by_prov_id( + provider_domain=mapping.provider_domain, + provider_item_id=mapping.item_id, + ): + return item + return None + + async def get_library_item_by_external_id( + self, external_id: str, external_id_type: ExternalID | None = None + ) -> ItemCls | None: + """Get the library item for the given external id.""" + query = f"{self.db_table}.external_ids LIKE :external_id_str" + if external_id_type: + external_id_str = f'%"{external_id_type}","{external_id}"%' + else: + external_id_str = f'%"{external_id}"%' + for item in await self._get_library_items_by_query( + extra_query_parts=[query], extra_query_params={"external_id_str": external_id_str} + ): + return item + return None + + async def get_library_item_by_external_ids( + self, external_ids: set[tuple[ExternalID, str]] + ) -> ItemCls | None: + """Get the library item for (one of) the given external ids.""" + for external_id_type, external_id in external_ids: + if match := await self.get_library_item_by_external_id(external_id, external_id_type): + return match + return None + + async def get_library_items_by_prov_id( + self, + provider_domain: str | None = None, + provider_instance: str | None = None, + provider_instance_id_or_domain: str | None = None, + provider_item_id: str | None = None, + limit: int = 500, + offset: int = 0, + ) -> list[ItemCls]: + """Fetch all records from library for given provider.""" + assert provider_instance_id_or_domain != "library" + assert provider_domain != "library" + assert provider_instance != "library" + subquery_parts: list[str] = [] + query_params: dict[str, Any] = {} + if provider_instance: + query_params = {"prov_id": provider_instance} + subquery_parts.append("provider_mappings.provider_instance = :prov_id") + elif provider_domain: + query_params = {"prov_id": provider_domain} + subquery_parts.append("provider_mappings.provider_domain = :prov_id") + else: + query_params = {"prov_id": provider_instance_id_or_domain} + subquery_parts.append( + "(provider_mappings.provider_instance = :prov_id " + "OR provider_mappings.provider_domain = :prov_id)" + ) + if provider_item_id: + subquery_parts.append("provider_mappings.provider_item_id = :item_id") + query_params["item_id"] = provider_item_id + subquery = f"SELECT item_id FROM provider_mappings WHERE {' AND '.join(subquery_parts)}" + query = f"WHERE {self.db_table}.item_id IN ({subquery})" + return await self._get_library_items_by_query( + limit=limit, offset=offset, extra_query_parts=[query], extra_query_params=query_params + ) + + async def iter_library_items_by_prov_id( + self, + provider_instance_id_or_domain: str, + provider_item_id: str | None = None, + ) -> AsyncGenerator[ItemCls, None]: + """Iterate all records from database for given provider.""" + limit: int = 500 + offset: int = 0 + while True: + next_items = await self.get_library_items_by_prov_id( + provider_instance_id_or_domain=provider_instance_id_or_domain, + provider_item_id=provider_item_id, + limit=limit, + offset=offset, + ) + for item in next_items: + yield item + if len(next_items) < limit: + break + offset += limit + + async def set_favorite(self, item_id: str | int, favorite: bool) -> None: + """Set the favorite bool on a database item.""" + db_id = int(item_id) # ensure integer + library_item = await self.get_library_item(db_id) + if library_item.favorite == favorite: + return + match = {"item_id": db_id} + await self.mass.music.database.update(self.db_table, match, {"favorite": favorite}) + library_item = await self.get_library_item(db_id) + self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) + + async def get_provider_item( + self, + item_id: str, + provider_instance_id_or_domain: str, + force_refresh: bool = False, + fallback: ItemMapping | ItemCls = None, + ) -> ItemCls: + """Return item details for the given provider item id.""" + if provider_instance_id_or_domain == "library": + return await self.get_library_item(item_id) + if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): + raise ProviderUnavailableError(f"{provider_instance_id_or_domain} is not available") + + cache_category = CacheCategory.MUSIC_PROVIDER_ITEM + cache_base_key = provider.lookup_key + cache_key = f"{self.media_type.value}.{item_id}" + if not force_refresh and ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ): + return self.item_cls.from_dict(cache) + if provider := self.mass.get_provider(provider_instance_id_or_domain): + with suppress(MediaNotFoundError): + if item := await provider.get_item(self.media_type, item_id): + await self.mass.cache.set( + cache_key, item.to_dict(), category=cache_category, base_key=cache_base_key + ) + return item + # if we reach this point all possibilities failed and the item could not be found. + # There is a possibility that the (streaming) provider changed the id of the item + # so we return the previous details (if we have any) marked as unavailable, so + # at least we have the possibility to sort out the new id through matching logic. + fallback = fallback or await self.get_library_item_by_prov_id( + item_id, provider_instance_id_or_domain + ) + if fallback and not (isinstance(fallback, ItemMapping) and self.item_cls in (Track, Album)): + # simply return the fallback item + # NOTE: we only accept ItemMapping as fallback for flat items + # so not for tracks and albums (which rely on other objects) + return fallback + # all options exhausted, we really can not find this item + msg = ( + f"{self.media_type.value}://{item_id} not " + f"found on provider {provider_instance_id_or_domain}" + ) + raise MediaNotFoundError(msg) + + async def add_provider_mapping( + self, item_id: str | int, provider_mapping: ProviderMapping + ) -> None: + """Add provider mapping to existing library item.""" + db_id = int(item_id) # ensure integer + library_item = await self.get_library_item(db_id) + # ignore if the mapping is already present + if provider_mapping in library_item.provider_mappings: + return + library_item.provider_mappings.add(provider_mapping) + await self._set_provider_mappings(db_id, library_item.provider_mappings) + + async def remove_provider_mapping( + self, item_id: str | int, provider_instance_id: str, provider_item_id: str + ) -> None: + """Remove provider mapping(s) from item.""" + db_id = int(item_id) # ensure integer + try: + library_item = await self.get_library_item(db_id) + except MediaNotFoundError: + # edge case: already deleted / race condition + return + library_item.provider_mappings = { + x + for x in library_item.provider_mappings + if x.provider_instance != provider_instance_id and x.item_id != provider_item_id + } + # update provider_mappings table + await self.mass.music.database.delete( + DB_TABLE_PROVIDER_MAPPINGS, + { + "media_type": self.media_type.value, + "item_id": db_id, + "provider_instance": provider_instance_id, + "provider_item_id": provider_item_id, + }, + ) + # cleanup playlog table + await self.mass.music.database.delete( + DB_TABLE_PLAYLOG, + { + "media_type": self.media_type.value, + "item_id": provider_item_id, + "provider": provider_instance_id, + }, + ) + if library_item.provider_mappings: + self.logger.debug( + "removed provider_mapping %s/%s from item id %s", + provider_instance_id, + provider_item_id, + db_id, + ) + self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) + else: + # remove item if it has no more providers + with suppress(AssertionError): + await self.remove_item_from_library(db_id) + + async def remove_provider_mappings(self, item_id: str | int, provider_instance_id: str) -> None: + """Remove all provider mappings from an item.""" + db_id = int(item_id) # ensure integer + try: + library_item = await self.get_library_item(db_id) + except MediaNotFoundError: + # edge case: already deleted / race condition + library_item = None + # update provider_mappings table + await self.mass.music.database.delete( + DB_TABLE_PROVIDER_MAPPINGS, + { + "media_type": self.media_type.value, + "item_id": db_id, + "provider_instance": provider_instance_id, + }, + ) + if library_item is None: + return + # update the item's provider mappings (and check if we still have any) + library_item.provider_mappings = { + x for x in library_item.provider_mappings if x.provider_instance != provider_instance_id + } + if library_item.provider_mappings: + self.logger.debug( + "removed all provider mappings for provider %s from item id %s", + provider_instance_id, + db_id, + ) + self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) + else: + # remove item if it has no more providers + with suppress(AssertionError): + await self.remove_item_from_library(db_id) + + async def dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> list[Track]: + """Return a list of base tracks to calculate a list of dynamic tracks.""" + ref_item = await self.get(item_id, provider_instance_id_or_domain) + for prov_mapping in ref_item.provider_mappings: + prov = self.mass.get_provider(prov_mapping.provider_instance) + if prov is None: + continue + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + continue + return await self._get_provider_dynamic_base_tracks( + prov_mapping.item_id, + prov_mapping.provider_instance, + ) + # Fallback to the default implementation + return await self._get_dynamic_tracks(ref_item) + + @abstractmethod + async def _add_library_item( + self, + item: ItemCls, + overwrite_existing: bool = False, + ) -> int: + """Add artist to library and return the database id.""" + + @abstractmethod + async def _update_library_item( + self, item_id: str | int, update: ItemCls, overwrite: bool = False + ) -> None: + """Update existing library record in the database.""" + + async def match_providers(self, db_item: ItemCls) -> None: + """ + Try to find match on all (streaming) providers for the provided (database) item. + + This is used to link objects of different providers/qualities together. + """ + + @abstractmethod + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> list[Track]: + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + + @abstractmethod + async def _get_dynamic_tracks(self, media_item: ItemCls, limit: int = 25) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + + async def _get_library_items_by_query( + self, + favorite: bool | None = None, + search: str | None = None, + limit: int = 500, + offset: int = 0, + order_by: str | None = None, + provider: str | None = None, + extra_query_parts: list[str] | None = None, + extra_query_params: dict[str, Any] | None = None, + extra_join_parts: list[str] | None = None, + ) -> list[ItemCls]: + """Fetch MediaItem records from database by building the query.""" + sql_query = self.base_query + query_params = extra_query_params or {} + query_parts: list[str] = extra_query_parts or [] + join_parts: list[str] = extra_join_parts or [] + # create special performant random query + if order_by and order_by.startswith("random"): + query_parts.append( + f"{self.db_table}.item_id in " + f"(SELECT item_id FROM {self.db_table} ORDER BY RANDOM() LIMIT {limit})" + ) + # handle search + if search: + query_params["search"] = f"%{search}%" + query_parts.append(f"{self.db_table}.name LIKE :search") + # handle favorite filter + if favorite is not None: + query_parts.append(f"{self.db_table}.favorite = :favorite") + query_params["favorite"] = favorite + # handle provider filter + if provider: + join_parts.append( + f"JOIN provider_mappings ON provider_mappings.item_id = {self.db_table}.item_id " + f"AND provider_mappings.media_type = '{self.media_type.value}' " + f"AND (provider_mappings.provider_instance = '{provider}' " + f"OR provider_mappings.provider_domain = '{provider}')" + ) + # prevent duplicate where statement + query_parts = [x[5:] if x.lower().startswith("where ") else x for x in query_parts] + # concetenate all join and/or where queries + if join_parts: + sql_query += f' {" ".join(join_parts)} ' + if query_parts: + sql_query += " WHERE " + " AND ".join(query_parts) + # build final query + sql_query += f" GROUP BY {self.db_table}.item_id" + if order_by: + if sort_key := SORT_KEYS.get(order_by): + sql_query += f" ORDER BY {sort_key}" + # return dbresult parsed to media item model + return [ + self.item_cls.from_dict(self._parse_db_row(db_row)) + for db_row in await self.mass.music.database.get_rows_from_query( + sql_query, query_params, limit=limit, offset=offset + ) + ] + + async def _set_provider_mappings( + self, + item_id: str | int, + provider_mappings: Iterable[ProviderMapping], + overwrite: bool = False, + ) -> None: + """Update the provider_items table for the media item.""" + db_id = int(item_id) # ensure integer + if overwrite: + # on overwrite, clear the provider_mappings table first + # this is done for filesystem provider changing the path (and thus item_id) + await self.mass.music.database.delete( + DB_TABLE_PROVIDER_MAPPINGS, + {"media_type": self.media_type.value, "item_id": db_id}, + ) + for provider_mapping in provider_mappings: + if not provider_mapping.provider_instance: + continue + await self.mass.music.database.insert_or_replace( + DB_TABLE_PROVIDER_MAPPINGS, + { + "media_type": self.media_type.value, + "item_id": db_id, + "provider_domain": provider_mapping.provider_domain, + "provider_instance": provider_mapping.provider_instance, + "provider_item_id": provider_mapping.item_id, + "available": provider_mapping.available, + "url": provider_mapping.url, + "audio_format": serialize_to_json(provider_mapping.audio_format), + "details": provider_mapping.details, + }, + ) + + @staticmethod + def _parse_db_row(db_row: Mapping) -> dict[str, Any]: + """Parse raw db Mapping into a dict.""" + db_row_dict = dict(db_row) + db_row_dict["provider"] = "library" + db_row_dict["favorite"] = bool(db_row_dict["favorite"]) + db_row_dict["item_id"] = str(db_row_dict["item_id"]) + + for key in JSON_KEYS: + if key not in db_row_dict: + continue + if not (raw_value := db_row_dict[key]): + continue + db_row_dict[key] = json_loads(raw_value) + + # copy track_album --> album + if track_album := db_row_dict.get("track_album"): + db_row_dict["album"] = track_album + db_row_dict["disc_number"] = track_album["disc_number"] + db_row_dict["track_number"] = track_album["track_number"] + # copy album image to itemmapping single image + if images := track_album.get("images"): + db_row_dict["album"]["image"] = next( + (x for x in images if x["type"] == "thumb"), None + ) + return db_row_dict diff --git a/music_assistant/controllers/media/playlists.py b/music_assistant/controllers/media/playlists.py new file mode 100644 index 00000000..5051c743 --- /dev/null +++ b/music_assistant/controllers/media/playlists.py @@ -0,0 +1,454 @@ +"""Manage MediaItems of type Playlist.""" + +from __future__ import annotations + +import random +import time +from collections.abc import AsyncGenerator +from typing import Any + +from music_assistant_models.enums import CacheCategory, MediaType, ProviderFeature, ProviderType +from music_assistant_models.errors import ( + InvalidDataError, + MediaNotFoundError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) +from music_assistant_models.helpers.uri import create_uri, parse_uri +from music_assistant_models.media_items import Playlist, PlaylistTrack, Track + +from music_assistant.constants import DB_TABLE_PLAYLISTS +from music_assistant.helpers.json import serialize_to_json +from music_assistant.models.music_provider import MusicProvider + +from .base import MediaControllerBase + + +class PlaylistController(MediaControllerBase[Playlist]): + """Controller managing MediaItems of type Playlist.""" + + db_table = DB_TABLE_PLAYLISTS + media_type = MediaType.PLAYLIST + item_cls = Playlist + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + # register (extra) api handlers + api_base = self.api_base + self.mass.register_api_command(f"music/{api_base}/create_playlist", self.create_playlist) + self.mass.register_api_command("music/playlists/playlist_tracks", self.tracks) + self.mass.register_api_command( + "music/playlists/add_playlist_tracks", self.add_playlist_tracks + ) + self.mass.register_api_command( + "music/playlists/remove_playlist_tracks", self.remove_playlist_tracks + ) + + async def tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + force_refresh: bool = False, + ) -> AsyncGenerator[PlaylistTrack, None]: + """Return playlist tracks for the given provider playlist id.""" + playlist = await self.get( + item_id, + provider_instance_id_or_domain, + ) + # a playlist can only have one provider so simply pick the first one + prov_map = next(x for x in playlist.provider_mappings) + cache_checksum = playlist.cache_checksum + # playlist tracks are not stored in the db, + # we always fetched them (cached) from the provider + page = 0 + while True: + tracks = await self._get_provider_playlist_tracks( + prov_map.item_id, + prov_map.provider_instance, + cache_checksum=cache_checksum, + page=page, + force_refresh=force_refresh, + ) + if not tracks: + break + for track in tracks: + yield track + page += 1 + + async def create_playlist( + self, name: str, provider_instance_or_domain: str | None = None + ) -> Playlist: + """Create new playlist.""" + # if provider is omitted, just pick builtin provider + if provider_instance_or_domain: + provider = self.mass.get_provider(provider_instance_or_domain) + if provider is None: + raise ProviderUnavailableError + else: + provider = self.mass.get_provider("builtin") + + # create playlist on the provider + playlist = await provider.create_playlist(name) + # add the new playlist to the library + return await self.add_item_to_library(playlist, False) + + async def add_playlist_tracks(self, db_playlist_id: str | int, uris: list[str]) -> None: # noqa: PLR0915 + """Add tracks to playlist.""" + db_id = int(db_playlist_id) # ensure integer + playlist = await self.get_library_item(db_id) + if not playlist: + msg = f"Playlist with id {db_id} not found" + raise MediaNotFoundError(msg) + if not playlist.is_editable: + msg = f"Playlist {playlist.name} is not editable" + raise InvalidDataError(msg) + + # grab all existing track ids in the playlist so we can check for duplicates + playlist_prov_map = next(iter(playlist.provider_mappings)) + playlist_prov = self.mass.get_provider(playlist_prov_map.provider_instance) + if not playlist_prov or not playlist_prov.available: + msg = f"Provider {playlist_prov_map.provider_instance} is not available" + raise ProviderUnavailableError(msg) + cur_playlist_track_ids = set() + cur_playlist_track_uris = set() + async for item in self.tracks(playlist.item_id, playlist.provider): + cur_playlist_track_uris.add(item.item_id) + cur_playlist_track_uris.add(item.uri) + + # work out the track id's that need to be added + # filter out duplicates and items that not exist on the provider. + ids_to_add: set[str] = set() + for uri in uris: + # skip if item already in the playlist + if uri in cur_playlist_track_uris: + self.logger.info( + "Not adding %s to playlist %s - it already exists", uri, playlist.name + ) + continue + + # parse uri for further processing + media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) + + # skip if item already in the playlist + if item_id in cur_playlist_track_ids: + self.logger.warning( + "Not adding %s to playlist %s - it already exists", uri, playlist.name + ) + continue + + # skip non-track items + # TODO: revisit this once we support audiobooks and podcasts ? + if media_type != MediaType.TRACK: + self.logger.warning( + "Not adding %s to playlist %s - not a track", uri, playlist.name + ) + continue + + # special: the builtin provider can handle uri's from all providers (with uri as id) + if provider_instance_id_or_domain != "library" and playlist_prov.domain == "builtin": + # note: we try not to add library uri's to the builtin playlists + # so we can survive db rebuilds + ids_to_add.add(uri) + self.logger.info( + "Adding %s to playlist %s", + uri, + playlist.name, + ) + continue + + # if target playlist is an exact provider match, we can add it + if provider_instance_id_or_domain != "library": + item_prov = self.mass.get_provider(provider_instance_id_or_domain) + if not item_prov or not item_prov.available: + self.logger.warning( + "Skip adding %s to playlist: Provider %s is not available", + uri, + provider_instance_id_or_domain, + ) + continue + if item_prov.lookup_key == playlist_prov.lookup_key: + ids_to_add.add(item_id) + continue + + # ensure we have a full (library) track (including all provider mappings) + full_track = await self.mass.music.tracks.get( + item_id, + provider_instance_id_or_domain, + recursive=provider_instance_id_or_domain != "library", + ) + track_prov_domains = {x.provider_domain for x in full_track.provider_mappings} + if ( + playlist_prov.domain != "builtin" + and playlist_prov.is_streaming_provider + and playlist_prov.domain not in track_prov_domains + ): + # try to match the track to the playlist provider + full_track.provider_mappings.update( + await self.mass.music.tracks.match_provider(playlist_prov, full_track, False) + ) + + # a track can contain multiple versions on the same provider + # simply sort by quality and just add the first available version + for track_version in sorted( + full_track.provider_mappings, key=lambda x: x.quality, reverse=True + ): + if not track_version.available: + continue + if track_version.item_id in cur_playlist_track_ids: + break # already existing in the playlist + item_prov = self.mass.get_provider(track_version.provider_instance) + if not item_prov: + continue + track_version_uri = create_uri( + MediaType.TRACK, + item_prov.lookup_key, + track_version.item_id, + ) + if track_version_uri in cur_playlist_track_uris: + self.logger.warning( + "Not adding %s to playlist %s - it already exists", + full_track.name, + playlist.name, + ) + break # already existing in the playlist + if playlist_prov.domain == "builtin": + # the builtin provider can handle uri's from all providers (with uri as id) + ids_to_add.add(track_version_uri) + self.logger.info( + "Adding %s to playlist %s", + full_track.name, + playlist.name, + ) + break + if item_prov.lookup_key == playlist_prov.lookup_key: + ids_to_add.add(track_version.item_id) + self.logger.info( + "Adding %s to playlist %s", + full_track.name, + playlist.name, + ) + break + else: + self.logger.warning( + "Can't add %s to playlist %s - it is not available on provider %s", + full_track.name, + playlist.name, + playlist_prov.name, + ) + + if not ids_to_add: + return + + # actually add the tracks to the playlist on the provider + await playlist_prov.add_playlist_tracks(playlist_prov_map.item_id, list(ids_to_add)) + # invalidate cache so tracks get refreshed + playlist.cache_checksum = str(time.time()) + await self.update_item_in_library(db_playlist_id, playlist) + + async def add_playlist_track(self, db_playlist_id: str | int, track_uri: str) -> None: + """Add (single) track to playlist.""" + await self.add_playlist_tracks(db_playlist_id, [track_uri]) + + async def remove_playlist_tracks( + self, db_playlist_id: str | int, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove multiple tracks from playlist.""" + db_id = int(db_playlist_id) # ensure integer + playlist = await self.get_library_item(db_id) + if not playlist: + msg = f"Playlist with id {db_id} not found" + raise MediaNotFoundError(msg) + if not playlist.is_editable: + msg = f"Playlist {playlist.name} is not editable" + raise InvalidDataError(msg) + for prov_mapping in playlist.provider_mappings: + provider = self.mass.get_provider(prov_mapping.provider_instance) + if ProviderFeature.PLAYLIST_TRACKS_EDIT not in provider.supported_features: + self.logger.warning( + "Provider %s does not support editing playlists", + prov_mapping.provider_domain, + ) + continue + await provider.remove_playlist_tracks(prov_mapping.item_id, positions_to_remove) + # invalidate cache so tracks get refreshed + playlist.cache_checksum = str(time.time()) + await self.update_item_in_library(db_playlist_id, playlist) + + async def _add_library_item(self, item: Playlist) -> int: + """Add a new record to the database.""" + db_id = await self.mass.music.database.insert( + self.db_table, + { + "name": item.name, + "sort_name": item.sort_name, + "owner": item.owner, + "is_editable": item.is_editable, + "favorite": item.favorite, + "metadata": serialize_to_json(item.metadata), + "external_ids": serialize_to_json(item.external_ids), + "cache_checksum": item.cache_checksum, + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, item.provider_mappings) + self.logger.debug("added %s to database (id: %s)", item.name, db_id) + return db_id + + async def _update_library_item( + self, item_id: int, update: Playlist, overwrite: bool = False + ) -> None: + """Update existing record in the database.""" + db_id = int(item_id) # ensure integer + cur_item = await self.get_library_item(db_id) + metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) + cur_item.external_ids.update(update.external_ids) + await self.mass.music.database.update( + self.db_table, + {"item_id": db_id}, + { + # always prefer name/owner from updated item here + "name": update.name, + "sort_name": update.sort_name + if (overwrite or update.name != cur_item.name) + else cur_item.sort_name, + "owner": update.owner or cur_item.owner, + "is_editable": update.is_editable, + "metadata": serialize_to_json(metadata), + "external_ids": serialize_to_json( + update.external_ids if overwrite else cur_item.external_ids + ), + "cache_checksum": update.cache_checksum or cur_item.cache_checksum, + }, + ) + # update/set provider_mappings table + provider_mappings = ( + update.provider_mappings + if overwrite + else {*cur_item.provider_mappings, *update.provider_mappings} + ) + await self._set_provider_mappings(db_id, provider_mappings, overwrite) + self.logger.debug("updated %s in database: (id %s)", update.name, db_id) + + async def _get_provider_playlist_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + cache_checksum: Any = None, + page: int = 0, + force_refresh: bool = False, + ) -> list[PlaylistTrack]: + """Return playlist tracks for the given provider playlist id.""" + assert provider_instance_id_or_domain != "library" + provider: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain) + if not provider: + return [] + # prefer cache items (if any) + cache_category = CacheCategory.MUSIC_PLAYLIST_TRACKS + cache_base_key = provider.lookup_key + cache_key = f"{item_id}.{page}" + if ( + not force_refresh + and ( + cache := await self.mass.cache.get( + cache_key, + checksum=cache_checksum, + category=cache_category, + base_key=cache_base_key, + ) + ) + is not None + ): + return [PlaylistTrack.from_dict(x) for x in cache] + # no items in cache (or force_refresh) - get listing from provider + items = await provider.get_playlist_tracks(item_id, page=page) + # store (serializable items) in cache + self.mass.create_task( + self.mass.cache.set( + cache_key, + [x.to_dict() for x in items], + checksum=cache_checksum, + category=cache_category, + base_key=cache_base_key, + ) + ) + for item in items: + # if this is a complete track object, pre-cache it as + # that will save us an (expensive) lookup later + if item.image and item.artist_str and item.album and provider.domain != "builtin": + await self.mass.cache.set( + f"track.{item_id}", + item.to_dict(), + category=CacheCategory.MUSIC_PROVIDER_ITEM, + base_key=provider.lookup_key, + ) + return items + + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ): + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + assert provider_instance_id_or_domain != "library" + playlist = await self.get(item_id, provider_instance_id_or_domain) + return [ + x + async for x in self.tracks(playlist.item_id, playlist.provider) + # filter out unavailable tracks + if x.available + ] + + async def _get_dynamic_tracks( + self, + media_item: Playlist, + limit: int = 25, + ) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + # check if we have any provider that supports dynamic tracks + # TODO: query metadata provider(s) (such as lastfm?) + # to get similar tracks (or tracks from similar artists) + for prov in self.mass.get_providers(ProviderType.MUSIC): + if ProviderFeature.SIMILAR_TRACKS in prov.supported_features: + break + else: + msg = "No Music Provider found that supports requesting similar tracks." + raise UnsupportedFeaturedException(msg) + + radio_items: list[Track] = [] + radio_item_titles: set[str] = set() + playlist_tracks = [x async for x in self.tracks(media_item.item_id, media_item.provider)] + random.shuffle(playlist_tracks) + for playlist_track in playlist_tracks: + # prefer library item if available so we can use all providers + if playlist_track.provider != "library" and ( + db_item := await self.mass.music.tracks.get_library_item_by_prov_id( + playlist_track.item_id, playlist_track.provider + ) + ): + playlist_track = db_item # noqa: PLW2901 + + if not playlist_track.available: + continue + # include base item in the list + radio_items.append(playlist_track) + radio_item_titles.add(playlist_track.name) + # now try to find similar tracks + for item_prov_mapping in playlist_track.provider_mappings: + if not (prov := self.mass.get_provider(item_prov_mapping.provider_instance)): + continue + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + continue + # fetch some similar tracks on this provider + for similar_track in await prov.get_similar_tracks( + prov_track_id=item_prov_mapping.item_id, limit=5 + ): + if similar_track.name not in radio_item_titles: + radio_items.append(similar_track) + radio_item_titles.add(similar_track.name) + continue + if len(radio_items) >= limit: + break + # Shuffle the final items list + random.shuffle(radio_items) + return radio_items diff --git a/music_assistant/controllers/media/radio.py b/music_assistant/controllers/media/radio.py new file mode 100644 index 00000000..d16e71e8 --- /dev/null +++ b/music_assistant/controllers/media/radio.py @@ -0,0 +1,120 @@ +"""Manage MediaItems of type Radio.""" + +from __future__ import annotations + +import asyncio + +from music_assistant_models.enums import MediaType +from music_assistant_models.media_items import Radio, Track + +from music_assistant.constants import DB_TABLE_RADIOS +from music_assistant.helpers.compare import loose_compare_strings +from music_assistant.helpers.json import serialize_to_json + +from .base import MediaControllerBase + + +class RadioController(MediaControllerBase[Radio]): + """Controller managing MediaItems of type Radio.""" + + db_table = DB_TABLE_RADIOS + media_type = MediaType.RADIO + item_cls = Radio + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + # register (extra) api handlers + api_base = self.api_base + self.mass.register_api_command(f"music/{api_base}/radio_versions", self.versions) + + async def versions( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> list[Radio]: + """Return all versions of a radio station we can find on all providers.""" + radio = await self.get(item_id, provider_instance_id_or_domain) + # perform a search on all provider(types) to collect all versions/variants + all_versions = { + prov_item.item_id: prov_item + for prov_items in await asyncio.gather( + *[ + self.search(radio.name, provider_domain) + for provider_domain in self.mass.music.get_unique_providers() + ] + ) + for prov_item in prov_items + if loose_compare_strings(radio.name, prov_item.name) + } + # make sure that the 'base' version is NOT included + for prov_version in radio.provider_mappings: + all_versions.pop(prov_version.item_id, None) + + # return the aggregated result + return all_versions.values() + + async def _add_library_item(self, item: Radio) -> int: + """Add a new item record to the database.""" + db_id = await self.mass.music.database.insert( + self.db_table, + { + "name": item.name, + "sort_name": item.sort_name, + "favorite": item.favorite, + "metadata": serialize_to_json(item.metadata), + "external_ids": serialize_to_json(item.external_ids), + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, item.provider_mappings) + self.logger.debug("added %s to database (id: %s)", item.name, db_id) + return db_id + + async def _update_library_item( + self, item_id: str | int, update: Radio, overwrite: bool = False + ) -> None: + """Update existing record in the database.""" + db_id = int(item_id) # ensure integer + cur_item = await self.get_library_item(db_id) + metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) + cur_item.external_ids.update(update.external_ids) + match = {"item_id": db_id} + await self.mass.music.database.update( + self.db_table, + match, + { + # always prefer name from updated item here + "name": update.name if overwrite else cur_item.name, + "sort_name": update.sort_name + if overwrite + else cur_item.sort_name or update.sort_name, + "metadata": serialize_to_json(metadata), + "external_ids": serialize_to_json( + update.external_ids if overwrite else cur_item.external_ids + ), + }, + ) + # update/set provider_mappings table + provider_mappings = ( + update.provider_mappings + if overwrite + else {*cur_item.provider_mappings, *update.provider_mappings} + ) + await self._set_provider_mappings(db_id, provider_mappings, overwrite) + self.logger.debug("updated %s in database: (id %s)", update.name, db_id) + + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + limit: int = 25, + ) -> list[Track]: + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + msg = "Dynamic tracks not supported for Radio MediaItem" + raise NotImplementedError(msg) + + async def _get_dynamic_tracks(self, media_item: Radio, limit: int = 25) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + msg = "Dynamic tracks not supported for Radio MediaItem" + raise NotImplementedError(msg) diff --git a/music_assistant/controllers/media/tracks.py b/music_assistant/controllers/media/tracks.py new file mode 100644 index 00000000..0fc1523a --- /dev/null +++ b/music_assistant/controllers/media/tracks.py @@ -0,0 +1,594 @@ +"""Manage MediaItems of type Track.""" + +from __future__ import annotations + +import urllib.parse +from collections.abc import Iterable +from contextlib import suppress +from typing import Any + +from music_assistant_models.enums import MediaType, ProviderFeature +from music_assistant_models.errors import ( + InvalidDataError, + MediaNotFoundError, + MusicAssistantError, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import ( + Album, + Artist, + ItemMapping, + ProviderMapping, + Track, + UniqueList, +) + +from music_assistant.constants import ( + DB_TABLE_ALBUM_TRACKS, + DB_TABLE_ALBUMS, + DB_TABLE_TRACK_ARTISTS, + DB_TABLE_TRACKS, +) +from music_assistant.helpers.compare import ( + compare_artists, + compare_media_item, + compare_track, + loose_compare_strings, +) +from music_assistant.helpers.json import serialize_to_json +from music_assistant.models.music_provider import MusicProvider + +from .base import MediaControllerBase + + +class TracksController(MediaControllerBase[Track]): + """Controller managing MediaItems of type Track.""" + + db_table = DB_TABLE_TRACKS + media_type = MediaType.TRACK + item_cls = Track + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + self.base_query = """ + SELECT + tracks.*, + (SELECT JSON_GROUP_ARRAY( + json_object( + 'item_id', provider_mappings.provider_item_id, + 'provider_domain', provider_mappings.provider_domain, + 'provider_instance', provider_mappings.provider_instance, + 'available', provider_mappings.available, + 'audio_format', json(provider_mappings.audio_format), + 'url', provider_mappings.url, + 'details', provider_mappings.details + )) FROM provider_mappings WHERE provider_mappings.item_id = tracks.item_id AND media_type = 'track') AS provider_mappings, + + (SELECT JSON_GROUP_ARRAY( + json_object( + 'item_id', artists.item_id, + 'provider', 'library', + 'name', artists.name, + 'sort_name', artists.sort_name, + 'media_type', 'artist' + )) FROM artists JOIN track_artists on track_artists.track_id = tracks.item_id WHERE artists.item_id = track_artists.artist_id) AS artists, + (SELECT + json_object( + 'item_id', albums.item_id, + 'provider', 'library', + 'name', albums.name, + 'sort_name', albums.sort_name, + 'media_type', 'album', + 'disc_number', album_tracks.disc_number, + 'track_number', album_tracks.track_number, + 'images', json_extract(albums.metadata, '$.images') + ) FROM albums WHERE albums.item_id = album_tracks.album_id) AS track_album + FROM tracks + LEFT JOIN album_tracks on album_tracks.track_id = tracks.item_id + """ # noqa: E501 + # register (extra) api handlers + api_base = self.api_base + self.mass.register_api_command(f"music/{api_base}/track_versions", self.versions) + self.mass.register_api_command(f"music/{api_base}/track_albums", self.albums) + self.mass.register_api_command(f"music/{api_base}/preview", self.get_preview_url) + + async def get( + self, + item_id: str, + provider_instance_id_or_domain: str, + recursive: bool = True, + album_uri: str | None = None, + ) -> Track: + """Return (full) details for a single media item.""" + track = await super().get( + item_id, + provider_instance_id_or_domain, + ) + if not recursive and album_uri is None: + # return early if we do not want recursive full details and no album uri is provided + return track + + # append full album details to full track item (resolve ItemMappings) + try: + if album_uri and (album := await self.mass.music.get_item_by_uri(album_uri)): + track.album = album + elif provider_instance_id_or_domain == "library": + # grab the first album this track is attached to + for album_track_row in await self.mass.music.database.get_rows( + DB_TABLE_ALBUM_TRACKS, {"track_id": int(item_id)}, limit=1 + ): + track.album = await self.mass.music.albums.get_library_item( + album_track_row["album_id"] + ) + elif isinstance(track.album, ItemMapping) or (track.album and not track.album.image): + track.album = await self.mass.music.albums.get( + track.album.item_id, track.album.provider, recursive=False + ) + except MusicAssistantError as err: + # edge case where playlist track has invalid albumdetails + self.logger.warning("Unable to fetch album details for %s - %s", track.uri, str(err)) + + if not recursive: + return track + + # append artist details to full track item (resolve ItemMappings) + track_artists = [] + for artist in track.artists: + if not isinstance(artist, ItemMapping): + track_artists.append(artist) + continue + try: + track_artists.append( + await self.mass.music.artists.get( + artist.item_id, + artist.provider, + ) + ) + except MusicAssistantError as err: + # edge case where playlist track has invalid artistdetails + self.logger.warning("Unable to fetch artist details %s - %s", artist.uri, str(err)) + track.artists = track_artists + return track + + async def library_items( + self, + favorite: bool | None = None, + search: str | None = None, + limit: int = 500, + offset: int = 0, + order_by: str = "sort_name", + provider: str | None = None, + extra_query: str | None = None, + extra_query_params: dict[str, Any] | None = None, + ) -> list[Track]: + """Get in-database tracks.""" + extra_query_params: dict[str, Any] = extra_query_params or {} + extra_query_parts: list[str] = [extra_query] if extra_query else [] + extra_join_parts: list[str] = [] + if search and " - " in search: + # handle combined artist + title search + artist_str, title_str = search.split(" - ", 1) + search = None + extra_query_parts.append("tracks.name LIKE :search_title") + extra_query_params["search_title"] = f"%{title_str}%" + # use join with artists table to filter on artist name + extra_join_parts.append( + "JOIN track_artists ON track_artists.track_id = tracks.item_id " + "JOIN artists ON artists.item_id = track_artists.artist_id " + "AND artists.name LIKE :search_artist" + ) + extra_query_params["search_artist"] = f"%{artist_str}%" + result = await self._get_library_items_by_query( + favorite=favorite, + search=search, + limit=limit, + offset=offset, + order_by=order_by, + provider=provider, + extra_query_parts=extra_query_parts, + extra_query_params=extra_query_params, + extra_join_parts=extra_join_parts, + ) + if search and len(result) < 25 and not offset: + # append artist items to result + extra_join_parts.append( + "JOIN track_artists ON track_artists.track_id = tracks.item_id " + "JOIN artists ON artists.item_id = track_artists.artist_id " + "AND artists.name LIKE :search_artist" + ) + extra_query_params["search_artist"] = f"%{search}%" + return result + await self._get_library_items_by_query( + favorite=favorite, + search=None, + limit=limit, + order_by=order_by, + provider=provider, + extra_query_parts=extra_query_parts, + extra_query_params=extra_query_params, + extra_join_parts=extra_join_parts, + ) + return result + + async def versions( + self, + item_id: str, + provider_instance_id_or_domain: str, + ) -> UniqueList[Track]: + """Return all versions of a track we can find on all providers.""" + track = await self.get(item_id, provider_instance_id_or_domain) + search_query = f"{track.artist_str} - {track.name}" + result: UniqueList[Track] = UniqueList() + for provider_id in self.mass.music.get_unique_providers(): + provider = self.mass.get_provider(provider_id) + if not provider: + continue + if not provider.library_supported(MediaType.TRACK): + continue + result.extend( + prov_item + for prov_item in await self.search(search_query, provider_id) + if loose_compare_strings(track.name, prov_item.name) + and compare_artists(prov_item.artists, track.artists, any_match=True) + # make sure that the 'base' version is NOT included + and not track.provider_mappings.intersection(prov_item.provider_mappings) + ) + return result + + async def albums( + self, + item_id: str, + provider_instance_id_or_domain: str, + in_library_only: bool = False, + ) -> UniqueList[Album]: + """Return all albums the track appears on.""" + full_track = await self.get(item_id, provider_instance_id_or_domain) + db_items = ( + await self.get_library_track_albums(full_track.item_id) + if full_track.provider == "library" + else [] + ) + # return all (unique) items from all providers + result: UniqueList[Album] = UniqueList(db_items) + if full_track.provider == "library" and in_library_only: + # return in-library items only + return result + # use search to get all items on the provider + search_query = f"{full_track.artist_str} - {full_track.name}" + # TODO: we could use musicbrainz info here to get a list of all releases known + unique_ids: set[str] = set() + for prov_item in (await self.mass.music.search(search_query, [MediaType.TRACK])).tracks: + if not loose_compare_strings(full_track.name, prov_item.name): + continue + if not prov_item.album: + continue + if not compare_artists(full_track.artists, prov_item.artists, any_match=True): + continue + unique_id = f"{prov_item.album.name}.{prov_item.album.version}" + if unique_id in unique_ids: + continue + unique_ids.add(unique_id) + # prefer db item + if db_item := await self.mass.music.albums.get_library_item_by_prov_id( + prov_item.album.item_id, prov_item.album.provider + ): + result.append(db_item) + elif not in_library_only: + result.append(prov_item.album) + return result + + async def remove_item_from_library(self, item_id: str | int) -> None: + """Delete record from the database.""" + db_id = int(item_id) # ensure integer + # delete entry(s) from albumtracks table + await self.mass.music.database.delete(DB_TABLE_ALBUM_TRACKS, {"track_id": db_id}) + # delete entry(s) from trackartists table + await self.mass.music.database.delete(DB_TABLE_TRACK_ARTISTS, {"track_id": db_id}) + # delete the track itself from db + await super().remove_item_from_library(db_id) + + async def get_preview_url(self, provider_instance_id_or_domain: str, item_id: str) -> str: + """Return url to short preview sample.""" + track = await self.get_provider_item(item_id, provider_instance_id_or_domain) + # prefer provider-provided preview + if preview := track.metadata.preview: + return preview + # fallback to a preview/sample hosted by our own webserver + enc_track_id = urllib.parse.quote(item_id) + return ( + f"{self.mass.webserver.base_url}/preview?" + f"provider={provider_instance_id_or_domain}&item_id={enc_track_id}" + ) + + async def get_library_track_albums( + self, + item_id: str | int, + ) -> list[Album]: + """Return all in-library albums for a track.""" + subquery = ( + f"SELECT album_id FROM {DB_TABLE_ALBUM_TRACKS} " + f"WHERE {DB_TABLE_ALBUM_TRACKS}.track_id = {item_id}" + ) + query = f"{DB_TABLE_ALBUMS}.item_id in ({subquery})" + return await self.mass.music.albums._get_library_items_by_query(extra_query_parts=[query]) + + async def match_providers(self, db_track: Track) -> None: + """Try to find matching track on all providers for the provided (database) track_id. + + This is used to link objects of different providers/qualities together. + """ + if db_track.provider != "library": + return # Matching only supported for database items + track_albums = await self.albums(db_track.item_id, db_track.provider) + for provider in self.mass.music.providers: + if ProviderFeature.SEARCH not in provider.supported_features: + continue + if not provider.is_streaming_provider: + # matching on unique providers is pointless as they push (all) their content to MA + continue + if not provider.library_supported(MediaType.TRACK): + continue + provider_matches = await self.match_provider( + provider, db_track, strict=True, ref_albums=track_albums + ) + for provider_mapping in provider_matches: + # 100% match, we update the db with the additional provider mapping(s) + await self.add_provider_mapping(db_track.item_id, provider_mapping) + db_track.provider_mappings.add(provider_mapping) + + async def match_provider( + self, + provider: MusicProvider, + ref_track: Track, + strict: bool = True, + ref_albums: list[Album] | None = None, + ) -> set[ProviderMapping]: + """Try to find matching track on given provider.""" + if ref_albums is None: + ref_albums = await self.albums(ref_track.item_id, ref_track.provider) + if ProviderFeature.SEARCH not in provider.supported_features: + raise UnsupportedFeaturedException("Provider does not support search") + if not provider.is_streaming_provider: + raise UnsupportedFeaturedException("Matching only possible for streaming providers") + self.logger.debug("Trying to match track %s on provider %s", ref_track.name, provider.name) + matches: set[ProviderMapping] = set() + for artist in ref_track.artists: + if matches: + break + search_str = f"{artist.name} - {ref_track.name}" + search_result = await self.search(search_str, provider.domain) + for search_result_item in search_result: + if not search_result_item.available: + continue + # do a basic compare first + if not compare_media_item(ref_track, search_result_item, strict=False): + continue + # we must fetch the full version, search results can be simplified objects + prov_track = await self.get_provider_item( + search_result_item.item_id, + search_result_item.provider, + fallback=search_result_item, + ) + if compare_track(ref_track, prov_track, strict=strict, track_albums=ref_albums): + matches.update(search_result_item.provider_mappings) + + if not matches: + self.logger.debug( + "Could not find match for Track %s on provider %s", + ref_track.name, + provider.name, + ) + return matches + + async def get_provider_similar_tracks( + self, item_id: str, provider_instance_id_or_domain: str, limit: int = 25 + ): + """Get a list of similar tracks from the provider, based on the track.""" + ref_item = await self.get(item_id, provider_instance_id_or_domain) + for prov_mapping in ref_item.provider_mappings: + prov = self.mass.get_provider(prov_mapping.provider_instance) + if prov is None: + continue + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + continue + # Grab similar tracks from the music provider + return await prov.get_similar_tracks(prov_track_id=prov_mapping.item_id, limit=limit) + return [] + + async def _get_provider_dynamic_base_tracks( + self, + item_id: str, + provider_instance_id_or_domain: str, + ): + """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + assert provider_instance_id_or_domain != "library" + return [await self.get(item_id, provider_instance_id_or_domain)] + + async def _get_dynamic_tracks( + self, + media_item: Track, + limit: int = 25, + ) -> list[Track]: + """Get dynamic list of tracks for given item, fallback/default implementation.""" + # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) + msg = "No Music Provider found that supports requesting similar tracks." + raise UnsupportedFeaturedException(msg) + + async def _add_library_item(self, item: Track) -> int: + """Add a new item record to the database.""" + if not isinstance(item, Track): + msg = "Not a valid Track object (ItemMapping can not be added to db)" + raise InvalidDataError(msg) + if not item.artists: + msg = "Track is missing artist(s)" + raise InvalidDataError(msg) + db_id = await self.mass.music.database.insert( + self.db_table, + { + "name": item.name, + "sort_name": item.sort_name, + "version": item.version, + "duration": item.duration, + "favorite": item.favorite, + "external_ids": serialize_to_json(item.external_ids), + "metadata": serialize_to_json(item.metadata), + }, + ) + # update/set provider_mappings table + await self._set_provider_mappings(db_id, item.provider_mappings) + # set track artist(s) + await self._set_track_artists(db_id, item.artists) + # handle track album + if item.album: + await self._set_track_album( + db_id=db_id, + album=item.album, + disc_number=getattr(item, "disc_number", 0), + track_number=getattr(item, "track_number", 0), + ) + self.logger.debug("added %s to database (id: %s)", item.name, db_id) + return db_id + + async def _update_library_item( + self, item_id: str | int, update: Track, overwrite: bool = False + ) -> None: + """Update Track record in the database, merging data.""" + db_id = int(item_id) # ensure integer + cur_item = await self.get_library_item(db_id) + metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) + cur_item.external_ids.update(update.external_ids) + await self.mass.music.database.update( + self.db_table, + {"item_id": db_id}, + { + "name": update.name if overwrite else cur_item.name, + "sort_name": update.sort_name + if overwrite + else cur_item.sort_name or update.sort_name, + "version": update.version if overwrite else cur_item.version or update.version, + "duration": update.duration if overwrite else cur_item.duration or update.duration, + "metadata": serialize_to_json(metadata), + "external_ids": serialize_to_json( + update.external_ids if overwrite else cur_item.external_ids + ), + }, + ) + # update/set provider_mappings table + provider_mappings = ( + update.provider_mappings + if overwrite + else {*cur_item.provider_mappings, *update.provider_mappings} + ) + await self._set_provider_mappings(db_id, provider_mappings, overwrite) + # set track artist(s) + artists = update.artists if overwrite else cur_item.artists + update.artists + await self._set_track_artists(db_id, artists, overwrite=overwrite) + # update/set track album + if update.album: + await self._set_track_album( + db_id=db_id, + album=update.album, + disc_number=update.disc_number or cur_item.disc_number, + track_number=update.track_number or cur_item.track_number, + overwrite=overwrite, + ) + self.logger.debug("updated %s in database: (id %s)", update.name, db_id) + + async def _set_track_album( + self, + db_id: int, + album: Album | ItemMapping, + disc_number: int, + track_number: int, + overwrite: bool = False, + ) -> None: + """ + Store Track Album info. + + A track can exist on multiple albums so we have a mapping table between + albums and tracks which stores the relation between the two and it also + stores the track and disc number of the track within an album. + For digital releases, the discnumber will be just 0 or 1. + Track number should start counting at 1. + """ + db_album: Album | ItemMapping = None + if album.provider == "library": + db_album = album + elif existing := await self.mass.music.albums.get_library_item_by_prov_id( + album.item_id, album.provider + ): + db_album = existing + + if not db_album or overwrite: + # ensure we have an actual album object + if isinstance(album, ItemMapping): + album = await self.mass.music.albums.get_provider_item( + album.item_id, album.provider, fallback=album + ) + with suppress(MediaNotFoundError, AssertionError, InvalidDataError): + db_album = await self.mass.music.albums.add_item_to_library( + album, + overwrite_existing=overwrite, + ) + if not db_album: + # this should not happen but streaming providers can be awful sometimes + self.logger.warning( + "Unable to resolve Album %s for track %s, " + "track will be added to the library without this album!", + album.uri, + db_id, + ) + return + # write (or update) record in album_tracks table + await self.mass.music.database.insert_or_replace( + DB_TABLE_ALBUM_TRACKS, + { + "track_id": db_id, + "album_id": int(db_album.item_id), + "disc_number": disc_number, + "track_number": track_number, + }, + ) + + async def _set_track_artists( + self, db_id: int, artists: Iterable[Artist | ItemMapping], overwrite: bool = False + ) -> None: + """Store Track Artists.""" + if overwrite: + # on overwrite, clear the track_artists table first + await self.mass.music.database.delete( + DB_TABLE_TRACK_ARTISTS, + { + "track_id": db_id, + }, + ) + artist_mappings: UniqueList[ItemMapping] = UniqueList() + for artist in artists: + mapping = await self._set_track_artist(db_id, artist=artist, overwrite=overwrite) + artist_mappings.append(mapping) + + async def _set_track_artist( + self, db_id: int, artist: Artist | ItemMapping, overwrite: bool = False + ) -> ItemMapping: + """Store Track Artist info.""" + db_artist: Artist | ItemMapping = None + if artist.provider == "library": + db_artist = artist + elif existing := await self.mass.music.artists.get_library_item_by_prov_id( + artist.item_id, artist.provider + ): + db_artist = existing + + if not db_artist or overwrite: + db_artist = await self.mass.music.artists.add_item_to_library( + artist, overwrite_existing=overwrite + ) + # write (or update) record in album_artists table + await self.mass.music.database.insert_or_replace( + DB_TABLE_TRACK_ARTISTS, + { + "track_id": db_id, + "artist_id": int(db_artist.item_id), + }, + ) + return ItemMapping.from_item(db_artist) diff --git a/music_assistant/controllers/metadata.py b/music_assistant/controllers/metadata.py new file mode 100644 index 00000000..2c69820e --- /dev/null +++ b/music_assistant/controllers/metadata.py @@ -0,0 +1,800 @@ +"""All logic for metadata retrieval.""" + +from __future__ import annotations + +import asyncio +import collections +import logging +import os +import random +import urllib.parse +from base64 import b64encode +from contextlib import suppress +from time import time +from typing import TYPE_CHECKING, cast +from uuid import uuid4 + +import aiofiles +from aiohttp import web +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( + AlbumType, + ConfigEntryType, + ImageType, + MediaType, + ProviderFeature, + ProviderType, +) +from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.media_items import ( + Album, + Artist, + ItemMapping, + MediaItemImage, + MediaItemType, + Playlist, + Track, +) + +from music_assistant.constants import ( + CONF_LANGUAGE, + DB_TABLE_ALBUMS, + DB_TABLE_ARTISTS, + DB_TABLE_PLAYLISTS, + VARIOUS_ARTISTS_MBID, + VARIOUS_ARTISTS_NAME, + VERBOSE_LOG_LEVEL, +) +from music_assistant.helpers.api import api_command +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.images import create_collage, get_image_thumb +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.core_controller import CoreController + +if TYPE_CHECKING: + from music_assistant_models.config_entries import CoreConfig + + from music_assistant.models.metadata_provider import MetadataProvider + from music_assistant.providers.musicbrainz import MusicbrainzProvider + +LOCALES = { + "af_ZA": "African", + "ar_AE": "Arabic (United Arab Emirates)", + "ar_EG": "Arabic (Egypt)", + "ar_SA": "Saudi Arabia", + "bg_BG": "Bulgarian", + "cs_CZ": "Czech", + "zh_CN": "Chinese", + "hr_HR": "Croatian", + "da_DK": "Danish", + "de_DE": "German", + "el_GR": "Greek", + "en_AU": "English (AU)", + "en_US": "English (US)", + "en_GB": "English (UK)", + "es_ES": "Spanish", + "et_EE": "Estonian", + "fi_FI": "Finnish", + "fr_FR": "French", + "hu_HU": "Hungarian", + "is_IS": "Icelandic", + "it_IT": "Italian", + "lt_LT": "Lithuanian", + "lv_LV": "Latvian", + "jp_JP": "Japanese", + "ko_KR": "Korean", + "nl_NL": "Dutch", + "nb_NO": "Norwegian Bokmål", + "pl_PL": "Polish", + "pt_PT": "Portuguese", + "ro_RO": "Romanian", + "ru_RU": "Russian", + "sk_SK": "Slovak", + "sl_SI": "Slovenian", + "sr_RS": "Serbian", + "sv_SE": "Swedish", + "tr_TR": "Turkish", + "uk_UA": "Ukrainian", +} + +DEFAULT_LANGUAGE = "en_US" +REFRESH_INTERVAL_ARTISTS = 60 * 60 * 24 * 90 # 90 days +REFRESH_INTERVAL_ALBUMS = 60 * 60 * 24 * 90 # 90 days +REFRESH_INTERVAL_TRACKS = 60 * 60 * 24 * 90 # 90 days +REFRESH_INTERVAL_PLAYLISTS = 60 * 60 * 24 * 7 # 7 days +PERIODIC_SCAN_INTERVAL = 60 * 60 * 24 # 1 day +CONF_ENABLE_ONLINE_METADATA = "enable_online_metadata" + + +class MetaDataController(CoreController): + """Several helpers to search and store metadata for mediaitems.""" + + domain: str = "metadata" + config: CoreConfig + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + self.cache = self.mass.cache + self._pref_lang: str | None = None + self.manifest.name = "Metadata controller" + self.manifest.description = ( + "Music Assistant's core controller which handles all metadata for music." + ) + self.manifest.icon = "book-information-variant" + self._lookup_jobs: MetadataLookupQueue = MetadataLookupQueue() + self._lookup_task: asyncio.Task | None = None + self._throttler = Throttler(1, 30) + self._missing_metadata_scan_task: asyncio.Task | None = None + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + return ( + ConfigEntry( + key=CONF_LANGUAGE, + type=ConfigEntryType.STRING, + label="Preferred language", + required=False, + default_value=DEFAULT_LANGUAGE, + description="Preferred language for metadata.\n\n" + "Note that English will always be used as fallback when content " + "in your preferred language is not available.", + options=tuple(ConfigValueOption(value, key) for key, value in LOCALES.items()), + ), + ConfigEntry( + key=CONF_ENABLE_ONLINE_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable metadata retrieval from online metadata providers", + required=False, + default_value=True, + description="Enable online metadata lookups.\n\n" + "This will allow Music Assistant to fetch additional metadata from (enabled) " + "metadata providers, such as The Audio DB and Fanart.tv.\n\n" + "Note that these online sources are only queried when no information is already " + "available from local files or the music providers and local artwork/metadata " + "will always have preference over online sources so consider metadata from online " + "sources as complementary only.\n\n" + "The retrieval of additional rich metadata is a process that is executed slowly " + "in the background to not overload these free services with requests. " + "You can speedup the process by storing the images and other metadata locally.", + ), + ) + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + self.config = config + if not self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + # silence PIL logger + logging.getLogger("PIL").setLevel(logging.WARNING) + # make sure that our directory with collage images exists + self._collage_images_dir = os.path.join(self.mass.storage_path, "collage_images") + if not await asyncio.to_thread(os.path.exists, self._collage_images_dir): + await asyncio.to_thread(os.mkdir, self._collage_images_dir) + self.mass.streams.register_dynamic_route("/imageproxy", self.handle_imageproxy) + # the lookup task is used to process metadata lookup jobs + self._lookup_task = self.mass.create_task(self._process_metadata_lookup_jobs()) + # just tun the scan for missing metadata once at startup + # TODO: allows to enable/disable this in the UI and configure interval/time + self._missing_metadata_scan_task = self.mass.create_task(self._scan_missing_metadata()) + + async def close(self) -> None: + """Handle logic on server stop.""" + if self._lookup_task and not self._lookup_task.done(): + self._lookup_task.cancel() + if self._missing_metadata_scan_task and not self._missing_metadata_scan_task.done(): + self._missing_metadata_scan_task.cancel() + self.mass.streams.unregister_dynamic_route("/imageproxy") + + @property + def providers(self) -> list[MetadataProvider]: + """Return all loaded/running MetadataProviders.""" + if TYPE_CHECKING: + return cast(list[MetadataProvider], self.mass.get_providers(ProviderType.METADATA)) + return self.mass.get_providers(ProviderType.METADATA) + + @property + def preferred_language(self) -> str: + """Return preferred language for metadata (as 2 letter language code 'en').""" + return self.locale.split("_")[0] + + @property + def locale(self) -> str: + """Return preferred language for metadata (as full locale code 'en_EN').""" + return self.mass.config.get_raw_core_config_value( + self.domain, CONF_LANGUAGE, DEFAULT_LANGUAGE + ) + + @api_command("metadata/set_default_preferred_language") + def set_default_preferred_language(self, lang: str) -> None: + """ + Set the (default) preferred language. + + Reasoning behind this is that the backend can not make a wise choice for the default, + so relies on some external source that knows better to set this info, like the frontend + or a streaming provider. + Can only be set once (by this call or the user). + """ + if self.mass.config.get_raw_core_config_value(self.domain, CONF_LANGUAGE): + return # already set + # prefer exact match + if lang in LOCALES: + self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, lang) + return + # try strict matching on either locale code or region + lang = lang.lower().replace("-", "_") + for locale_code, lang_name in LOCALES.items(): + if lang in (locale_code.lower(), lang_name.lower()): + self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, locale_code) + return + # attempt loose match on language code or region code + for lang_part in (lang[:2], lang[:-2]): + for locale_code in tuple(LOCALES): + language_code, region_code = locale_code.lower().split("_", 1) + if lang_part in (language_code, region_code): + self.mass.config.set_raw_core_config_value( + self.domain, CONF_LANGUAGE, locale_code + ) + return + # if we reach this point, we couldn't match the language + self.logger.warning("%s is not a valid language", lang) + + @api_command("metadata/update_metadata") + async def update_metadata( + self, item: str | MediaItemType, force_refresh: bool = False + ) -> MediaItemType: + """Get/update extra/enhanced metadata for/on given MediaItem.""" + if isinstance(item, str): + item = await self.mass.music.get_item_by_uri(item) + if item.provider != "library": + # this shouldn't happen but just in case. + raise RuntimeError("Metadata can only be updated for library items") + # just in case it was in the queue, prevent duplicate lookups + self._lookup_jobs.pop(item.uri) + async with self._throttler: + if item.media_type == MediaType.ARTIST: + await self._update_artist_metadata(item, force_refresh=force_refresh) + if item.media_type == MediaType.ALBUM: + await self._update_album_metadata(item, force_refresh=force_refresh) + if item.media_type == MediaType.TRACK: + await self._update_track_metadata(item, force_refresh=force_refresh) + if item.media_type == MediaType.PLAYLIST: + await self._update_playlist_metadata(item, force_refresh=force_refresh) + return item + + def schedule_update_metadata(self, uri: str) -> None: + """Schedule metadata update for given MediaItem uri.""" + if "library" not in uri: + return + with suppress(asyncio.QueueFull): + self._lookup_jobs.put_nowait(uri) + + async def get_image_data_for_item( + self, + media_item: MediaItemType, + img_type: ImageType = ImageType.THUMB, + size: int = 0, + ) -> bytes | None: + """Get image data for given MedaItem.""" + img_path = await self.get_image_url_for_item( + media_item=media_item, + img_type=img_type, + ) + if not img_path: + return None + return await self.get_thumbnail(img_path, size) + + async def get_image_url_for_item( + self, + media_item: MediaItemType, + img_type: ImageType = ImageType.THUMB, + resolve: bool = True, + ) -> str | None: + """Get url to image for given media media_item.""" + if not media_item: + return None + if isinstance(media_item, ItemMapping): + media_item = await self.mass.music.get_item_by_uri(media_item.uri) + if media_item and media_item.metadata.images: + for img in media_item.metadata.images: + if img.type != img_type: + continue + if img.remotely_accessible and not resolve: + continue + if img.remotely_accessible and resolve: + return self.get_image_url(img) + return img.path + + # retry with track's album + if media_item.media_type == MediaType.TRACK and media_item.album: + return await self.get_image_url_for_item(media_item.album, img_type, resolve) + + # try artist instead for albums + if media_item.media_type == MediaType.ALBUM and media_item.artists: + return await self.get_image_url_for_item(media_item.artists[0], img_type, resolve) + + # last resort: track artist(s) + if media_item.media_type == MediaType.TRACK and media_item.artists: + for artist in media_item.artists: + return await self.get_image_url_for_item(artist, img_type, resolve) + + return None + + def get_image_url( + self, + image: MediaItemImage, + size: int = 0, + prefer_proxy: bool = False, + image_format: str = "png", + ) -> str: + """Get (proxied) URL for MediaItemImage.""" + if not image.remotely_accessible or prefer_proxy or size: + # return imageproxy url for images that need to be resolved + # the original path is double encoded + encoded_url = urllib.parse.quote(urllib.parse.quote(image.path)) + return ( + f"{self.mass.streams.base_url}/imageproxy?path={encoded_url}" + f"&provider={image.provider}&size={size}&fmt={image_format}" + ) + return image.path + + async def get_thumbnail( + self, + path: str, + provider: str, + size: int | None = None, + base64: bool = False, + image_format: str = "png", + ) -> bytes | str: + """Get/create thumbnail image for path (image url or local path).""" + if not self.mass.get_provider(provider) and not path.startswith("http"): + raise ProviderUnavailableError + thumbnail = await get_image_thumb( + self.mass, path, size=size, provider=provider, image_format=image_format + ) + if base64: + enc_image = b64encode(thumbnail).decode() + thumbnail = f"data:image/{image_format};base64,{enc_image}" + return thumbnail + + async def handle_imageproxy(self, request: web.Request) -> web.Response: + """Handle request for image proxy.""" + path = request.query["path"] + provider = request.query.get("provider", "builtin") + if provider in ("url", "file", "http"): + # temporary for backwards compatibility + provider = "builtin" + size = int(request.query.get("size", "0")) + image_format = request.query.get("fmt", "png") + if not self.mass.get_provider(provider) and not path.startswith("http"): + return web.Response(status=404) + if "%" in path: + # assume (double) encoded url, decode it + path = urllib.parse.unquote(path) + with suppress(FileNotFoundError): + image_data = await self.get_thumbnail( + path, size=size, provider=provider, image_format=image_format + ) + # we set the cache header to 1 year (forever) + # assuming that images do not/rarely change + return web.Response( + body=image_data, + headers={"Cache-Control": "max-age=31536000", "Access-Control-Allow-Origin": "*"}, + content_type=f"image/{image_format}", + ) + return web.Response(status=404) + + async def create_collage_image( + self, + images: list[MediaItemImage], + img_path: str, + fanart: bool = False, + ) -> MediaItemImage | None: + """Create collage thumb/fanart image for (in-library) playlist.""" + if len(images) < 8 and fanart or len(images) < 3: + # require at least some images otherwise this does not make a lot of sense + return None + # limit to 50 images to prevent we're going OOM + if len(images) > 50: + images = random.sample(images, 50) + else: + random.shuffle(images) + try: + # create collage thumb from playlist tracks + # if playlist has no default image (e.g. a local playlist) + dimensions = (2500, 1750) if fanart else (1500, 1500) + img_data = await create_collage(self.mass, images, dimensions) + # always overwrite existing path + async with aiofiles.open(img_path, "wb") as _file: + await _file.write(img_data) + del img_data + return MediaItemImage( + type=ImageType.FANART if fanart else ImageType.THUMB, + path=img_path, + provider="builtin", + remotely_accessible=False, + ) + except Exception as err: + self.logger.warning( + "Error while creating playlist image: %s", + str(err), + exc_info=err if self.logger.isEnabledFor(10) else None, + ) + return None + + async def _update_artist_metadata(self, artist: Artist, force_refresh: bool = False) -> None: + """Get/update rich metadata for an artist.""" + # collect metadata from all (online) music + metadata providers + # NOTE: we only do/allow this every REFRESH_INTERVAL + needs_refresh = (time() - (artist.metadata.last_refresh or 0)) > REFRESH_INTERVAL_ARTISTS + if not (force_refresh or needs_refresh): + return + + self.logger.debug("Updating metadata for Artist %s", artist.name) + unique_keys: set[str] = set() + + # collect (local) metadata from all local providers + local_provs = get_global_cache_value("non_streaming_providers") + if TYPE_CHECKING: + local_provs = cast(set[str], local_provs) + + # ensure the item is matched to all providers + await self.mass.music.artists.match_providers(artist) + + # collect metadata from all [music] providers + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + for prov_mapping in sorted( + artist.provider_mappings, key=lambda x: x.priority, reverse=True + ): + if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: + continue + if prov.lookup_key in unique_keys: + continue + if prov.lookup_key not in local_provs: + unique_keys.add(prov.lookup_key) + with suppress(MediaNotFoundError): + prov_item = await self.mass.music.artists.get_provider_item( + prov_mapping.item_id, prov_mapping.provider_instance + ) + artist.metadata.update(prov_item.metadata) + + # The musicbrainz ID is mandatory for all metadata lookups + if not artist.mbid: + # TODO: Use a global cache/proxy for the MB lookups to save on API calls + if mbid := await self._get_artist_mbid(artist): + artist.mbid = mbid + + # collect metadata from all (online)[metadata] providers + # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls + if self.config.get_value(CONF_ENABLE_ONLINE_METADATA) and artist.mbid: + for provider in self.providers: + if ProviderFeature.ARTIST_METADATA not in provider.supported_features: + continue + if metadata := await provider.get_artist_metadata(artist): + artist.metadata.update(metadata) + self.logger.debug( + "Fetched metadata for Artist %s on provider %s", + artist.name, + provider.name, + ) + # update final item in library database + # set timestamp, used to determine when this function was last called + artist.metadata.last_refresh = int(time()) + await self.mass.music.artists.update_item_in_library(artist.item_id, artist) + + async def _update_album_metadata(self, album: Album, force_refresh: bool = False) -> None: + """Get/update rich metadata for an album.""" + # collect metadata from all (online) music + metadata providers + # NOTE: we only do/allow this every REFRESH_INTERVAL + needs_refresh = (time() - (album.metadata.last_refresh or 0)) > REFRESH_INTERVAL_ALBUMS + if not (force_refresh or needs_refresh): + return + + self.logger.debug("Updating metadata for Album %s", album.name) + + # ensure the item is matched to all providers (will also get other quality versions) + await self.mass.music.albums.match_providers(album) + + # collect metadata from all [music] providers + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + unique_keys: set[str] = set() + local_provs = get_global_cache_value("non_streaming_providers") + if TYPE_CHECKING: + local_provs = cast(set[str], local_provs) + for prov_mapping in sorted(album.provider_mappings, key=lambda x: x.priority, reverse=True): + if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: + continue + if prov.lookup_key in unique_keys: + continue + if prov.lookup_key not in local_provs: + unique_keys.add(prov.lookup_key) + with suppress(MediaNotFoundError): + prov_item = await self.mass.music.albums.get_provider_item( + prov_mapping.item_id, prov_mapping.provider_instance + ) + album.metadata.update(prov_item.metadata) + if album.year is None and prov_item.year: + album.year = prov_item.year + if album.album_type == AlbumType.UNKNOWN: + album.album_type = prov_item.album_type + + # collect metadata from all (online) [metadata] providers + # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls + if self.config.get_value(CONF_ENABLE_ONLINE_METADATA): + for provider in self.providers: + if ProviderFeature.ALBUM_METADATA not in provider.supported_features: + continue + if metadata := await provider.get_album_metadata(album): + album.metadata.update(metadata) + self.logger.debug( + "Fetched metadata for Album %s on provider %s", + album.name, + provider.name, + ) + # update final item in library database + # set timestamp, used to determine when this function was last called + album.metadata.last_refresh = int(time()) + await self.mass.music.albums.update_item_in_library(album.item_id, album) + + async def _update_track_metadata(self, track: Track, force_refresh: bool = False) -> None: + """Get/update rich metadata for a track.""" + # collect metadata from all (online) music + metadata providers + # NOTE: we only do/allow this every REFRESH_INTERVAL + needs_refresh = (time() - (track.metadata.last_refresh or 0)) > REFRESH_INTERVAL_TRACKS + if not (force_refresh or needs_refresh): + return + + self.logger.debug("Updating metadata for Track %s", track.name) + + # ensure the item is matched to all providers (will also get other quality versions) + await self.mass.music.tracks.match_providers(track) + + # collect metadata from all [music] providers + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + unique_keys: set[str] = set() + local_provs = get_global_cache_value("non_streaming_providers") + if TYPE_CHECKING: + local_provs = cast(set[str], local_provs) + for prov_mapping in sorted(track.provider_mappings, key=lambda x: x.priority, reverse=True): + if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: + continue + if prov.lookup_key in unique_keys: + continue + unique_keys.add(prov.lookup_key) + with suppress(MediaNotFoundError): + prov_item = await self.mass.music.tracks.get_provider_item( + prov_mapping.item_id, prov_mapping.provider_instance + ) + track.metadata.update(prov_item.metadata) + + # collect metadata from all [metadata] providers + # there is only little metadata available for tracks so we only fetch metadata + # from other sources if the force flag is set + if force_refresh and self.config.get_value(CONF_ENABLE_ONLINE_METADATA): + for provider in self.providers: + if ProviderFeature.TRACK_METADATA not in provider.supported_features: + continue + if metadata := await provider.get_track_metadata(track): + track.metadata.update(metadata) + self.logger.debug( + "Fetched metadata for Track %s on provider %s", + track.name, + provider.name, + ) + # set timestamp, used to determine when this function was last called + track.metadata.last_refresh = int(time()) + # update final item in library database + await self.mass.music.tracks.update_item_in_library(track.item_id, track) + + async def _update_playlist_metadata( + self, playlist: Playlist, force_refresh: bool = False + ) -> None: + """Get/update rich metadata for a playlist.""" + # collect metadata + create collage images + # NOTE: we only do/allow this every REFRESH_INTERVAL + needs_refresh = ( + time() - (playlist.metadata.last_refresh or 0) + ) > REFRESH_INTERVAL_PLAYLISTS + if not (force_refresh or needs_refresh): + return + self.logger.debug("Updating metadata for Playlist %s", playlist.name) + playlist.metadata.genres = set() + all_playlist_tracks_images: list[MediaItemImage] = [] + playlist_genres: dict[str, int] = {} + # retrieve metadata for the playlist from the tracks (such as genres etc.) + # TODO: retrieve style/mood ? + async for track in self.mass.music.playlists.tracks(playlist.item_id, playlist.provider): + if ( + track.image + and track.image not in all_playlist_tracks_images + and ( + track.image.provider in ("url", "builtin", "http") + or self.mass.get_provider(track.image.provider) + ) + ): + all_playlist_tracks_images.append(track.image) + if track.metadata.genres: + genres = track.metadata.genres + elif track.album and isinstance(track.album, Album) and track.album.metadata.genres: + genres = track.album.metadata.genres + else: + genres = set() + for genre in genres: + if genre not in playlist_genres: + playlist_genres[genre] = 0 + playlist_genres[genre] += 1 + await asyncio.sleep(0) # yield to eventloop + + playlist_genres_filtered = {genre for genre, count in playlist_genres.items() if count > 5} + playlist_genres_filtered = list(playlist_genres_filtered)[:8] + playlist.metadata.genres.update(playlist_genres_filtered) + # create collage images + cur_images = playlist.metadata.images or [] + new_images = [] + # thumb image + thumb_image = next((x for x in cur_images if x.type == ImageType.THUMB), None) + if not thumb_image or self._collage_images_dir in thumb_image.path: + thumb_image_path = ( + thumb_image.path + if thumb_image + else os.path.join(self._collage_images_dir, f"{uuid4().hex}_thumb.jpg") + ) + if collage_thumb_image := await self.create_collage_image( + all_playlist_tracks_images, thumb_image_path + ): + new_images.append(collage_thumb_image) + elif thumb_image: + # just use old image + new_images.append(thumb_image) + # fanart image + fanart_image = next((x for x in cur_images if x.type == ImageType.FANART), None) + if not fanart_image or self._collage_images_dir in fanart_image.path: + fanart_image_path = ( + fanart_image.path + if fanart_image + else os.path.join(self._collage_images_dir, f"{uuid4().hex}_fanart.jpg") + ) + if collage_fanart_image := await self.create_collage_image( + all_playlist_tracks_images, fanart_image_path, fanart=True + ): + new_images.append(collage_fanart_image) + elif fanart_image: + # just use old image + new_images.append(fanart_image) + playlist.metadata.images = new_images + # set timestamp, used to determine when this function was last called + playlist.metadata.last_refresh = int(time()) + # update final item in library database + await self.mass.music.playlists.update_item_in_library(playlist.item_id, playlist) + + async def _get_artist_mbid(self, artist: Artist) -> str | None: + """Fetch musicbrainz id by performing search using the artist name, albums and tracks.""" + if artist.mbid: + return artist.mbid + if compare_strings(artist.name, VARIOUS_ARTISTS_NAME): + return VARIOUS_ARTISTS_MBID + + musicbrainz: MusicbrainzProvider = self.mass.get_provider("musicbrainz") + if TYPE_CHECKING: + musicbrainz = cast(MusicbrainzProvider, musicbrainz) + # first try with resource URL (e.g. streaming provider share URL) + for prov_mapping in artist.provider_mappings: + if prov_mapping.url and prov_mapping.url.startswith("http"): + if mb_artist := await musicbrainz.get_artist_details_by_resource_url( + prov_mapping.url + ): + return mb_artist.id + + # start lookup of musicbrainz id using artist name, albums and tracks + ref_albums = await self.mass.music.artists.albums( + artist.item_id, artist.provider, in_library_only=False + ) + ref_tracks = await self.mass.music.artists.tracks( + artist.item_id, artist.provider, in_library_only=False + ) + # try with (strict) ref track(s), using recording id + for ref_track in ref_tracks: + if mb_artist := await musicbrainz.get_artist_details_by_track(artist.name, ref_track): + return mb_artist.id + # try with (strict) ref album(s), using releasegroup id + for ref_album in ref_albums: + if mb_artist := await musicbrainz.get_artist_details_by_album(artist.name, ref_album): + return mb_artist.id + # last restort: track matching by name + for ref_track in ref_tracks: + if not ref_track.album: + continue + if result := await musicbrainz.search( + artistname=artist.name, + albumname=ref_track.album.name, + trackname=ref_track.name, + trackversion=ref_track.version, + ): + return result[0].id + + # lookup failed + ref_albums_str = "/".join(x.name for x in ref_albums) or "none" + ref_tracks_str = "/".join(x.name for x in ref_tracks) or "none" + self.logger.debug( + "Unable to get musicbrainz ID for artist %s\n" + " - using lookup-album(s): %s\n" + " - using lookup-track(s): %s\n", + artist.name, + ref_albums_str, + ref_tracks_str, + ) + return None + + async def _process_metadata_lookup_jobs(self) -> None: + """Task to process metadata lookup jobs.""" + while True: + item_uri = await self._lookup_jobs.get() + try: + item = await self.mass.music.get_item_by_uri(item_uri) + await self.update_metadata(item) + except Exception as err: + self.logger.error( + "Error while updating metadata for %s: %s", + item_uri, + str(err), + exc_info=err if self.logger.isEnabledFor(10) else None, + ) + + async def _scan_missing_metadata(self) -> None: + """Scanner for (missing) metadata, periodically in the background.""" + self._periodic_scan = None + # Scan for missing artist images + self.logger.debug("Start lookup for missing artist images...") + query = ( + f"json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') ISNULL " + f"AND (json_extract({DB_TABLE_ARTISTS}.metadata,'$.images') ISNULL " + f"OR json_extract({DB_TABLE_ARTISTS}.metadata,'$.images') = '[]')" + ) + for artist in await self.mass.music.artists.library_items(extra_query=query): + self.schedule_update_metadata(artist.uri) + + # Scan for missing album images + self.logger.debug("Start lookup for missing album images...") + query = ( + f"json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') ISNULL " + f"AND (json_extract({DB_TABLE_ALBUMS}.metadata,'$.images') ISNULL " + f"OR json_extract({DB_TABLE_ALBUMS}.metadata,'$.images') = '[]')" + ) + for album in await self.mass.music.albums.library_items( + limit=50, order_by="random", extra_query=query + ): + self.schedule_update_metadata(album.uri) + + # Force refresh playlist metadata every refresh interval + # this will e.g. update the playlist image and genres if the tracks have changed + timestamp = int(time() - REFRESH_INTERVAL_PLAYLISTS) + query = ( + f"json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') ISNULL " + f"OR json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') < {timestamp}" + ) + for playlist in await self.mass.music.playlists.library_items( + limit=10, order_by="random", extra_query=query + ): + self.schedule_update_metadata(playlist.uri) + + +class MetadataLookupQueue(asyncio.Queue): + """Representation of a queue for metadata lookups.""" + + def _init(self, maxlen: int = 100): + self._queue: collections.deque[str] = collections.deque(maxlen=maxlen) + + def _put(self, item: str) -> None: + if item not in self._queue: + self._queue.append(item) + + def pop(self, item: str) -> None: + """Remove item from queue.""" + self._queue.remove(item) diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py new file mode 100644 index 00000000..b8c92578 --- /dev/null +++ b/music_assistant/controllers/music.py @@ -0,0 +1,1416 @@ +"""MusicController: Orchestrates all data from music providers and sync to internal database.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import shutil +from contextlib import suppress +from itertools import zip_longest +from math import inf +from typing import TYPE_CHECKING, Final, cast + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ( + CacheCategory, + ConfigEntryType, + EventType, + MediaType, + ProviderFeature, + ProviderType, +) +from music_assistant_models.errors import ( + InvalidProviderID, + InvalidProviderURI, + MediaNotFoundError, + MusicAssistantError, + ProviderUnavailableError, +) +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.helpers.uri import parse_uri +from music_assistant_models.media_items import ( + BrowseFolder, + ItemMapping, + MediaItemType, + SearchResults, +) +from music_assistant_models.provider import ProviderInstance, SyncTask + +from music_assistant.constants import ( + DB_TABLE_ALBUM_ARTISTS, + DB_TABLE_ALBUM_TRACKS, + DB_TABLE_ALBUMS, + DB_TABLE_ARTISTS, + DB_TABLE_LOUDNESS_MEASUREMENTS, + DB_TABLE_PLAYLISTS, + DB_TABLE_PLAYLOG, + DB_TABLE_PROVIDER_MAPPINGS, + DB_TABLE_RADIOS, + DB_TABLE_SETTINGS, + DB_TABLE_TRACK_ARTISTS, + DB_TABLE_TRACKS, + PROVIDERS_WITH_SHAREABLE_URLS, +) +from music_assistant.helpers.api import api_command +from music_assistant.helpers.database import DatabaseConnection +from music_assistant.helpers.datetime import utc_timestamp +from music_assistant.helpers.util import TaskManager +from music_assistant.models.core_controller import CoreController + +from .media.albums import AlbumsController +from .media.artists import ArtistsController +from .media.playlists import PlaylistController +from .media.radio import RadioController +from .media.tracks import TracksController + +if TYPE_CHECKING: + from music_assistant_models.config_entries import CoreConfig + + from music_assistant.models.music_provider import MusicProvider + +CONF_RESET_DB = "reset_db" +DEFAULT_SYNC_INTERVAL = 3 * 60 # default sync interval in minutes +CONF_SYNC_INTERVAL = "sync_interval" +CONF_DELETED_PROVIDERS = "deleted_providers" +CONF_ADD_LIBRARY_ON_PLAY = "add_library_on_play" +DB_SCHEMA_VERSION: Final[int] = 9 + + +class MusicController(CoreController): + """Several helpers around the musicproviders.""" + + domain: str = "music" + database: DatabaseConnection | None = None + config: CoreConfig + + def __init__(self, *args, **kwargs) -> None: + """Initialize class.""" + super().__init__(*args, **kwargs) + self.cache = self.mass.cache + self.artists = ArtistsController(self.mass) + self.albums = AlbumsController(self.mass) + self.tracks = TracksController(self.mass) + self.radio = RadioController(self.mass) + self.playlists = PlaylistController(self.mass) + self.in_progress_syncs: list[SyncTask] = [] + self._sync_lock = asyncio.Lock() + self.manifest.name = "Music controller" + self.manifest.description = ( + "Music Assistant's core controller which manages all music from all providers." + ) + self.manifest.icon = "archive-music" + self._sync_task: asyncio.Task | None = None + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + entries = ( + ConfigEntry( + key=CONF_SYNC_INTERVAL, + type=ConfigEntryType.INTEGER, + range=(5, 720), + default_value=DEFAULT_SYNC_INTERVAL, + label="Sync interval", + description="Interval (in minutes) that a (delta) sync " + "of all providers should be performed.", + ), + ConfigEntry( + key=CONF_ADD_LIBRARY_ON_PLAY, + type=ConfigEntryType.BOOLEAN, + default_value=False, + label="Add item to the library as soon as its played", + description="Automatically add a track or radio station to " + "the library when played (if its not already in the library).", + ), + ConfigEntry( + key=CONF_RESET_DB, + type=ConfigEntryType.ACTION, + label="Reset library database", + description="This will issue a full reset of the library " + "database and trigger a full sync. Only use this option as a last resort " + "if you are seeing issues with the library database.", + category="advanced", + ), + ) + if action == CONF_RESET_DB: + await self._reset_database() + await self.mass.cache.clear() + self.start_sync() + entries = ( + *entries, + ConfigEntry( + key=CONF_RESET_DB, + type=ConfigEntryType.LABEL, + label="The database has been reset.", + ), + ) + return entries + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + self.config = config + # setup library database + await self._setup_database() + sync_interval = config.get_value(CONF_SYNC_INTERVAL) + self.logger.info("Using a sync interval of %s minutes.", sync_interval) + # make sure to finish any removal jobs + for removed_provider in self.mass.config.get_raw_core_config_value( + self.domain, CONF_DELETED_PROVIDERS, [] + ): + await self.cleanup_provider(removed_provider) + self._schedule_sync() + + async def close(self) -> None: + """Cleanup on exit.""" + if self._sync_task and not self._sync_task.done(): + self._sync_task.cancel() + await self.database.close() + + @property + def providers(self) -> list[MusicProvider]: + """Return all loaded/running MusicProviders (instances).""" + return self.mass.get_providers(ProviderType.MUSIC) + + @api_command("music/sync") + def start_sync( + self, + media_types: list[MediaType] | None = None, + providers: list[str] | None = None, + ) -> None: + """Start running the sync of (all or selected) musicproviders. + + media_types: only sync these media types. None for all. + providers: only sync these provider instances. None for all. + """ + if media_types is None: + media_types = MediaType.ALL + if providers is None: + providers = [x.instance_id for x in self.providers] + + for provider in self.providers: + if provider.instance_id not in providers: + continue + self._start_provider_sync(provider, media_types) + + @api_command("music/synctasks") + def get_running_sync_tasks(self) -> list[SyncTask]: + """Return list with providers that are currently (scheduled for) syncing.""" + return self.in_progress_syncs + + @api_command("music/search") + async def search( + self, + search_query: str, + media_types: list[MediaType] = MediaType.ALL, + limit: int = 25, + library_only: bool = False, + ) -> SearchResults: + """Perform global search for media items on all providers. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: number of items to return in the search (per type). + """ + if not media_types: + media_types = MediaType.ALL + # Check if the search query is a streaming provider public shareable URL + try: + media_type, provider_instance_id_or_domain, item_id = await parse_uri( + search_query, validate_id=True + ) + except InvalidProviderURI: + pass + except InvalidProviderID as err: + self.logger.warning("%s", str(err)) + return SearchResults() + else: + if provider_instance_id_or_domain in PROVIDERS_WITH_SHAREABLE_URLS: + try: + item = await self.get_item( + media_type=media_type, + item_id=item_id, + provider_instance_id_or_domain=provider_instance_id_or_domain, + ) + except MusicAssistantError as err: + self.logger.warning("%s", str(err)) + return SearchResults() + else: + if media_type == MediaType.ARTIST: + return SearchResults(artists=[item]) + elif media_type == MediaType.ALBUM: + return SearchResults(albums=[item]) + elif media_type == MediaType.TRACK: + return SearchResults(tracks=[item]) + elif media_type == MediaType.PLAYLIST: + return SearchResults(playlists=[item]) + else: + return SearchResults() + + # include results from library + all (unique) music providers + search_providers = [] if library_only else self.get_unique_providers() + results_per_provider: list[SearchResults] = await asyncio.gather( + self.search_library(search_query, media_types, limit=limit), + *[ + self.search_provider( + search_query, + provider_instance, + media_types, + limit=limit, + ) + for provider_instance in search_providers + ], + ) + # return result from all providers while keeping index + # so the result is sorted as each provider delivered + return SearchResults( + artists=[ + item + for sublist in zip_longest(*[x.artists for x in results_per_provider]) + for item in sublist + if item is not None + ][:limit], + albums=[ + item + for sublist in zip_longest(*[x.albums for x in results_per_provider]) + for item in sublist + if item is not None + ][:limit], + tracks=[ + item + for sublist in zip_longest(*[x.tracks for x in results_per_provider]) + for item in sublist + if item is not None + ][:limit], + playlists=[ + item + for sublist in zip_longest(*[x.playlists for x in results_per_provider]) + for item in sublist + if item is not None + ][:limit], + radio=[ + item + for sublist in zip_longest(*[x.radio for x in results_per_provider]) + for item in sublist + if item is not None + ][:limit], + ) + + async def search_provider( + self, + search_query: str, + provider_instance_id_or_domain: str, + media_types: list[MediaType], + limit: int = 10, + ) -> SearchResults: + """Perform search on given provider. + + :param search_query: Search query + :param provider_instance_id_or_domain: instance_id or domain of the provider + to perform the search on. + :param media_types: A list of media_types to include. + :param limit: number of items to return in the search (per type). + """ + prov = self.mass.get_provider(provider_instance_id_or_domain) + if not prov: + return SearchResults() + if ProviderFeature.SEARCH not in prov.supported_features: + return SearchResults() + + # create safe search string + search_query = search_query.replace("/", " ").replace("'", "") + + # prefer cache items (if any) + media_types_str = ",".join(media_types) + cache_category = CacheCategory.MUSIC_SEARCH + cache_base_key = prov.lookup_key + cache_key = f"{search_query}.{limit}.{media_types_str}" + + if prov.is_streaming_provider and ( + cache := await self.mass.cache.get( + cache_key, category=cache_category, base_key=cache_base_key + ) + ): + return SearchResults.from_dict(cache) + # no items in cache - get listing from provider + result = await prov.search( + search_query, + media_types, + limit, + ) + # store (serializable items) in cache + if prov.is_streaming_provider: + self.mass.create_task( + self.mass.cache.set( + cache_key, + result.to_dict(), + expiration=86400 * 7, + category=cache_category, + base_key=cache_base_key, + ) + ) + return result + + async def search_library( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 10, + ) -> SearchResults: + """Perform search on the library. + + :param search_query: Search query + :param media_types: A list of media_types to include. + :param limit: number of items to return in the search (per type). + """ + result = SearchResults() + for media_type in media_types: + ctrl = self.get_controller(media_type) + search_results = await ctrl.search(search_query, "library", limit=limit) + if search_results: + if media_type == MediaType.ARTIST: + result.artists = search_results + elif media_type == MediaType.ALBUM: + result.albums = search_results + elif media_type == MediaType.TRACK: + result.tracks = search_results + elif media_type == MediaType.PLAYLIST: + result.playlists = search_results + elif media_type == MediaType.RADIO: + result.radio = search_results + return result + + @api_command("music/browse") + async def browse(self, path: str | None = None) -> list[MediaItemType]: + """Browse Music providers.""" + if not path or path == "root": + # root level; folder per provider + root_items: list[MediaItemType] = [] + for prov in self.providers: + if ProviderFeature.BROWSE not in prov.supported_features: + continue + root_items.append( + BrowseFolder( + item_id="root", + provider=prov.domain, + path=f"{prov.instance_id}://", + uri=f"{prov.instance_id}://", + name=prov.name, + ) + ) + return root_items + + # provider level + prepend_items: list[MediaItemType] = [] + provider_instance, sub_path = path.split("://", 1) + prov = self.mass.get_provider(provider_instance) + # handle regular provider listing, always add back folder first + if not prov or not sub_path: + prepend_items.append( + BrowseFolder(item_id="root", provider="library", path="root", name="..") + ) + if not prov: + return prepend_items + else: + back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1]) + prepend_items.append( + BrowseFolder(item_id="back", provider=provider_instance, path=back_path, name="..") + ) + # limit -1 to account for the prepended items + prov_items = await prov.browse(path=path) + return prepend_items + prov_items + + @api_command("music/recently_played_items") + async def recently_played( + self, limit: int = 10, media_types: list[MediaType] | None = None + ) -> list[MediaItemType]: + """Return a list of the last played items.""" + if media_types is None: + media_types = MediaType.ALL + media_types_str = "(" + ",".join(f'"{x}"' for x in media_types) + ")" + query = ( + f"SELECT * FROM {DB_TABLE_PLAYLOG} WHERE media_type " + f"in {media_types_str} ORDER BY timestamp DESC" + ) + db_rows = await self.mass.music.database.get_rows_from_query(query, limit=limit) + result: list[MediaItemType] = [] + for db_row in db_rows: + if db_row["provider"] not in get_global_cache_value("unique_providers", []): + continue + with suppress(MediaNotFoundError, ProviderUnavailableError): + media_type = MediaType(db_row["media_type"]) + ctrl = self.get_controller(media_type) + item = await ctrl.get( + db_row["item_id"], + db_row["provider"], + ) + result.append(item) + return result + + @api_command("music/item_by_uri") + async def get_item_by_uri(self, uri: str) -> MediaItemType: + """Fetch MediaItem by uri.""" + media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) + return await self.get_item( + media_type=media_type, + item_id=item_id, + provider_instance_id_or_domain=provider_instance_id_or_domain, + ) + + @api_command("music/item") + async def get_item( + self, + media_type: MediaType, + item_id: str, + provider_instance_id_or_domain: str, + ) -> MediaItemType: + """Get single music item by id and media type.""" + if provider_instance_id_or_domain == "database": + # backwards compatibility - to remove when 2.0 stable is released + provider_instance_id_or_domain = "library" + if provider_instance_id_or_domain == "builtin": + # handle special case of 'builtin' MusicProvider which allows us to play regular url's + return await self.mass.get_provider("builtin").parse_item(item_id) + ctrl = self.get_controller(media_type) + return await ctrl.get( + item_id=item_id, + provider_instance_id_or_domain=provider_instance_id_or_domain, + ) + + async def get_library_item_by_prov_id( + self, + media_type: MediaType, + item_id: str, + provider_instance_id_or_domain: str, + ) -> MediaItemType | None: + """Get single library music item by id and media type.""" + ctrl = self.get_controller(media_type) + return await ctrl.get_library_item_by_prov_id( + item_id=item_id, + provider_instance_id_or_domain=provider_instance_id_or_domain, + ) + + @api_command("music/favorites/add_item") + async def add_item_to_favorites( + self, + item: str | MediaItemType, + ) -> None: + """Add an item to the favorites.""" + if isinstance(item, str): + item = await self.get_item_by_uri(item) + # ensure item is added to streaming provider library + if ( + (provider := self.mass.get_provider(item.provider)) + and provider.is_streaming_provider + and provider.library_edit_supported(item.media_type) + ): + await provider.library_add(item) + # make sure we have a full library item + # a favorite must always be in the library + full_item = await self.get_item( + item.media_type, + item.item_id, + item.provider, + ) + if full_item.provider != "library": + full_item = await self.add_item_to_library(full_item) + # set favorite in library db + ctrl = self.get_controller(item.media_type) + await ctrl.set_favorite( + full_item.item_id, + True, + ) + + @api_command("music/favorites/remove_item") + async def remove_item_from_favorites( + self, + media_type: MediaType, + library_item_id: str | int, + ) -> None: + """Remove (library) item from the favorites.""" + ctrl = self.get_controller(media_type) + await ctrl.set_favorite( + library_item_id, + False, + ) + + @api_command("music/library/remove_item") + async def remove_item_from_library( + self, media_type: MediaType, library_item_id: str | int + ) -> None: + """ + Remove item from the library. + + Destructive! Will remove the item and all dependants. + """ + ctrl = self.get_controller(media_type) + item = await ctrl.get_library_item(library_item_id) + # remove from all providers + for provider_mapping in item.provider_mappings: + if prov_controller := self.mass.get_provider(provider_mapping.provider_instance): + # we simply try to remove it on the provider library + # NOTE that the item may not be in the provider's library at all + # so we need to be a bit forgiving here + with suppress(NotImplementedError): + await prov_controller.library_remove(provider_mapping.item_id, item.media_type) + await ctrl.remove_item_from_library(library_item_id) + + @api_command("music/library/add_item") + async def add_item_to_library( + self, item: str | MediaItemType, overwrite_existing: bool = False + ) -> MediaItemType: + """Add item (uri or mediaitem) to the library.""" + if isinstance(item, str): + item = await self.get_item_by_uri(item) + if isinstance(item, ItemMapping): + item = await self.get_item( + item.media_type, + item.item_id, + item.provider, + ) + # add to provider(s) library first + for prov_mapping in item.provider_mappings: + provider = self.mass.get_provider(prov_mapping.provider_instance) + if provider.library_edit_supported(item.media_type): + prov_item = item + prov_item.provider = prov_mapping.provider_instance + prov_item.item_id = prov_mapping.item_id + await provider.library_add(prov_item) + # add (or overwrite) to library + ctrl = self.get_controller(item.media_type) + library_item = await ctrl.add_item_to_library(item, overwrite_existing) + # perform full metadata scan (and provider match) + await self.mass.metadata.update_metadata(library_item, overwrite_existing) + return library_item + + async def refresh_items(self, items: list[MediaItemType]) -> None: + """Refresh MediaItems to force retrieval of full info and matches. + + Creates background tasks to process the action. + """ + async with TaskManager(self.mass) as tg: + for media_item in items: + tg.create_task(self.refresh_item(media_item)) + + @api_command("music/refresh_item") + async def refresh_item( + self, + media_item: str | MediaItemType, + ) -> MediaItemType | None: + """Try to refresh a mediaitem by requesting it's full object or search for substitutes.""" + if isinstance(media_item, str): + # media item uri given + media_item = await self.get_item_by_uri(media_item) + + media_type = media_item.media_type + ctrl = self.get_controller(media_type) + library_id = media_item.item_id if media_item.provider == "library" else None + + available_providers = get_global_cache_value("available_providers") + if TYPE_CHECKING: + available_providers = cast(set[str], available_providers) + + # fetch the first (available) provider item + for prov_mapping in sorted( + media_item.provider_mappings, key=lambda x: x.priority, reverse=True + ): + if not self.mass.get_provider(prov_mapping.provider_instance): + # ignore unavailable providers + continue + with suppress(MediaNotFoundError): + media_item = await ctrl.get_provider_item( + prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True + ) + provider = media_item.provider + item_id = media_item.item_id + break + else: + # try to find a substitute using search + searchresult = await self.search(media_item.name, [media_item.media_type], 20) + if media_item.media_type == MediaType.ARTIST: + result = searchresult.artists + elif media_item.media_type == MediaType.ALBUM: + result = searchresult.albums + elif media_item.media_type == MediaType.TRACK: + result = searchresult.tracks + elif media_item.media_type == MediaType.PLAYLIST: + result = searchresult.playlists + else: + result = searchresult.radio + for item in result: + if item == media_item or item.provider == "library": + continue + if item.available: + provider = item.provider + item_id = item.item_id + break + else: + # raise if we didn't find a substitute + raise MediaNotFoundError(f"Could not find a substitute for {media_item.name}") + # fetch full (provider) item + media_item = await ctrl.get_provider_item(item_id, provider, force_refresh=True) + # update library item if needed (including refresh of the metadata etc.) + if library_id is None: + return media_item + library_item = await ctrl.update_item_in_library(library_id, media_item, overwrite=True) + if library_item.media_type == MediaType.ALBUM: + # update (local) album tracks + for album_track in await self.albums.tracks( + library_item.item_id, library_item.provider, True + ): + for prov_mapping in album_track.provider_mappings: + if not (prov := self.mass.get_provider(prov_mapping.provider_instance)): + continue + if prov.is_streaming_provider: + continue + with suppress(MediaNotFoundError): + prov_track = await prov.get_track(prov_mapping.item_id) + await self.mass.music.tracks.update_item_in_library( + album_track.item_id, prov_track + ) + + await self.mass.metadata.update_metadata(library_item, force_refresh=True) + return library_item + + async def set_loudness( + self, + item_id: str, + provider_instance_id_or_domain: str, + loudness: float, + album_loudness: float | None = None, + media_type: MediaType = MediaType.TRACK, + ) -> None: + """Store (EBU-R128) Integrated Loudness Measurement for a mediaitem in db.""" + if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): + return + values = { + "item_id": item_id, + "media_type": media_type.value, + "provider": provider.lookup_key, + "loudness": loudness, + } + if album_loudness is not None: + values["loudness_album"] = album_loudness + await self.database.insert_or_replace(DB_TABLE_LOUDNESS_MEASUREMENTS, values) + + async def get_loudness( + self, + item_id: str, + provider_instance_id_or_domain: str, + media_type: MediaType = MediaType.TRACK, + ) -> tuple[float, float] | 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 + db_row = await self.database.get_row( + DB_TABLE_LOUDNESS_MEASUREMENTS, + { + "item_id": item_id, + "media_type": media_type.value, + "provider": provider.lookup_key, + }, + ) + if db_row and db_row["loudness"] != inf and db_row["loudness"] != -inf: + return (db_row["loudness"], db_row["loudness_album"]) + + return None + + async def mark_item_played( + self, media_type: MediaType, item_id: str, provider_instance_id_or_domain: str + ) -> None: + """Mark item as played in playlog.""" + timestamp = utc_timestamp() + + if ( + provider_instance_id_or_domain.startswith("builtin") + and media_type != MediaType.PLAYLIST + ): + # we deliberately skip builtin provider items as those are often + # one-off items like TTS or some sound effect etc. + return + + if provider_instance_id_or_domain == "library": + prov_key = "library" + elif prov := self.mass.get_provider(provider_instance_id_or_domain): + prov_key = prov.lookup_key + else: + prov_key = provider_instance_id_or_domain + + # update generic playlog table + await self.database.insert( + DB_TABLE_PLAYLOG, + { + "item_id": item_id, + "provider": prov_key, + "media_type": media_type.value, + "timestamp": timestamp, + }, + allow_replace=True, + ) + + # also update playcount in library table + ctrl = self.get_controller(media_type) + db_item = await ctrl.get_library_item_by_prov_id(item_id, provider_instance_id_or_domain) + if ( + not db_item + and media_type in (MediaType.TRACK, MediaType.RADIO) + and self.mass.config.get_raw_core_config_value(self.domain, CONF_ADD_LIBRARY_ON_PLAY) + ): + # handle feature to add to the lib on playback + full_item = await ctrl.get(item_id, provider_instance_id_or_domain) + db_item = await ctrl.add_item_to_library(full_item) + + if db_item: + await self.database.execute( + f"UPDATE {ctrl.db_table} SET play_count = play_count + 1, " + f"last_played = {timestamp} WHERE item_id = {db_item.item_id}" + ) + await self.database.commit() + + def get_controller( + self, media_type: MediaType + ) -> ( + ArtistsController + | AlbumsController + | TracksController + | RadioController + | PlaylistController + ): + """Return controller for MediaType.""" + if media_type == MediaType.ARTIST: + return self.artists + if media_type == MediaType.ALBUM: + return self.albums + if media_type == MediaType.TRACK: + return self.tracks + if media_type == MediaType.RADIO: + return self.radio + if media_type == MediaType.PLAYLIST: + return self.playlists + return None + + def get_unique_providers(self) -> set[str]: + """ + Return all unique MusicProvider instance ids. + + This will return all filebased instances but only one instance + for streaming providers. + """ + instances = set() + domains = set() + for provider in self.providers: + if provider.domain not in domains or not provider.is_streaming_provider: + instances.add(provider.instance_id) + domains.add(provider.domain) + return instances + + def _start_provider_sync( + self, provider: ProviderInstance, media_types: tuple[MediaType, ...] + ) -> None: + """Start sync task on provider and track progress.""" + # check if we're not already running a sync task for this provider/mediatype + for sync_task in self.in_progress_syncs: + if sync_task.provider_instance != provider.instance_id: + continue + for media_type in media_types: + if media_type in sync_task.media_types: + self.logger.debug( + "Skip sync task for %s because another task is already in progress", + provider.name, + ) + return + + async def run_sync() -> None: + # Wrap the provider sync into a lock to prevent + # race conditions when multiple providers are syncing at the same time. + async with self._sync_lock: + await provider.sync_library(media_types) + # precache playlist tracks + if MediaType.PLAYLIST in media_types: + for playlist in await self.playlists.library_items(provider=provider.instance_id): + async for _ in self.playlists.tracks(playlist.item_id, playlist.provider): + pass + + # we keep track of running sync tasks + task = self.mass.create_task(run_sync()) + sync_spec = SyncTask( + provider_domain=provider.domain, + provider_instance=provider.instance_id, + media_types=media_types, + task=task, + ) + self.in_progress_syncs.append(sync_spec) + + self.mass.signal_event(EventType.SYNC_TASKS_UPDATED, data=self.in_progress_syncs) + + def on_sync_task_done(task: asyncio.Task) -> None: + self.in_progress_syncs.remove(sync_spec) + if task.cancelled(): + return + if task_err := task.exception(): + self.logger.warning( + "Sync task for %s completed with errors", + provider.name, + exc_info=task_err if self.logger.isEnabledFor(10) else None, + ) + else: + self.logger.info("Sync task for %s completed", provider.name) + self.mass.signal_event(EventType.SYNC_TASKS_UPDATED, data=self.in_progress_syncs) + # schedule db cleanup after sync + if not self.in_progress_syncs: + self.mass.create_task(self._cleanup_database()) + + task.add_done_callback(on_sync_task_done) + + async def cleanup_provider(self, provider_instance: str) -> None: + """Cleanup provider records from the database.""" + if provider_instance.startswith(("filesystem", "jellyfin", "plex", "opensubsonic")): + # removal of a local provider can become messy very fast due to the relations + # such as images pointing at the files etc. so we just reset the whole db + self.logger.warning( + "Removal of local provider detected, issuing full database reset..." + ) + await self._reset_database() + return + deleted_providers = self.mass.config.get_raw_core_config_value( + self.domain, CONF_DELETED_PROVIDERS, [] + ) + # we add the provider to this hidden config setting just to make sure that + # we can survive this over a restart to make sure that entries are cleaned up + if provider_instance not in deleted_providers: + deleted_providers.append(provider_instance) + self.mass.config.set_raw_core_config_value( + self.domain, CONF_DELETED_PROVIDERS, deleted_providers + ) + self.mass.config.save(True) + + # always clear cache when a provider is removed + await self.mass.cache.clear() + + # cleanup media items from db matched to deleted provider + self.logger.info( + "Removing provider %s from library, this can take a a while...", provider_instance + ) + errors = 0 + for ctrl in ( + # order is important here to recursively cleanup bottom up + self.mass.music.radio, + self.mass.music.playlists, + self.mass.music.tracks, + self.mass.music.albums, + self.mass.music.artists, + # run main controllers twice to rule out relations + self.mass.music.tracks, + self.mass.music.albums, + self.mass.music.artists, + ): + query = ( + f"SELECT item_id FROM {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE media_type = '{ctrl.media_type}' " + f"AND provider_instance = '{provider_instance}'" + ) + for db_row in await self.database.get_rows_from_query(query, limit=100000): + try: + await ctrl.remove_provider_mappings(db_row["item_id"], provider_instance) + except Exception as err: + # we dont want the whole removal process to stall on one item + # so in case of an unexpected error, we log and move on. + self.logger.warning( + "Error while removing %s: %s", + db_row["item_id"], + str(err), + exc_info=err if self.logger.isEnabledFor(logging.DEBUG) else None, + ) + errors += 1 + + # remove all orphaned items (not in provider mappings table anymore) + query = ( + f"SELECT item_id FROM {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE provider_instance = '{provider_instance}'" + ) + if remaining_items_count := await self.database.get_count_from_query(query): + errors += remaining_items_count + + # cleanup playlog table + await self.mass.music.database.delete( + DB_TABLE_PLAYLOG, + { + "provider": provider_instance, + }, + ) + + if errors == 0: + # cleanup successful, remove from the deleted_providers setting + self.logger.info("Provider %s removed from library", provider_instance) + deleted_providers.remove(provider_instance) + self.mass.config.set_raw_core_config_value( + self.domain, CONF_DELETED_PROVIDERS, deleted_providers + ) + else: + self.logger.warning( + "Provider %s was not not fully removed from library", provider_instance + ) + + def _schedule_sync(self) -> None: + """Schedule the periodic sync.""" + self.start_sync() + sync_interval = self.config.get_value(CONF_SYNC_INTERVAL) + # we reschedule ourselves right after execution + # NOTE: sync_interval is stored in minutes, we need seconds + self.mass.loop.call_later(sync_interval * 60, self._schedule_sync) + + async def _cleanup_database(self) -> None: + """Perform database cleanup/maintenance.""" + self.logger.debug("Performing database cleanup...") + # Remove playlog entries older than 90 days + await self.database.delete_where_query( + DB_TABLE_PLAYLOG, f"timestamp < strftime('%s','now') - {3600 * 24 * 90}" + ) + # db tables cleanup + for ctrl in (self.albums, self.artists, self.tracks, self.playlists, self.radio): + # Provider mappings where the db item is removed + query = ( + f"item_id not in (SELECT item_id from {ctrl.db_table}) " + f"AND media_type = '{ctrl.media_type}'" + ) + await self.database.delete_where_query(DB_TABLE_PROVIDER_MAPPINGS, query) + # Orphaned db items + query = ( + f"item_id not in (SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE media_type = '{ctrl.media_type}')" + ) + await self.database.delete_where_query(ctrl.db_table, query) + # Cleanup removed db items from the playlog + where_clause = ( + f"media_type = '{ctrl.media_type}' AND provider = 'library' " + f"AND item_id not in (select item_id from {ctrl.db_table})" + ) + await self.mass.music.database.delete_where_query(DB_TABLE_PLAYLOG, where_clause) + self.logger.debug("Database cleanup done") + + async def _setup_database(self) -> None: + """Initialize database.""" + db_path = os.path.join(self.mass.storage_path, "library.db") + self.database = DatabaseConnection(db_path) + await self.database.setup() + + # always create db tables if they don't exist to prevent errors trying to access them later + await self.__create_database_tables() + try: + if db_row := await self.database.get_row(DB_TABLE_SETTINGS, {"key": "version"}): + prev_version = int(db_row["value"]) + else: + prev_version = 0 + except (KeyError, ValueError): + prev_version = 0 + + if prev_version not in (0, DB_SCHEMA_VERSION): + # db version mismatch - we need to do a migration + # make a backup of db file + db_path_backup = db_path + ".backup" + await asyncio.to_thread(shutil.copyfile, db_path, db_path_backup) + + # handle db migration from previous schema(s) to this one + try: + await self.__migrate_database(prev_version) + except Exception as err: + # if the migration fails completely we reset the db + # so the user at least can have a working situation back + # a backup file is made with the previous version + self.logger.error( + "Database migration failed - starting with a fresh library database, " + "a full rescan will be performed, this can take a while!", + ) + if not isinstance(err, MusicAssistantError): + self.logger.exception(err) + + await self.database.close() + await asyncio.to_thread(os.remove, db_path) + self.database = DatabaseConnection(db_path) + await self.database.setup() + await self.mass.cache.clear() + await self.__create_database_tables() + + # store current schema version + await self.database.insert_or_replace( + DB_TABLE_SETTINGS, + {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"}, + ) + # create indexes and triggers if needed + await self.__create_database_indexes() + await self.__create_database_triggers() + # compact db + self.logger.debug("Compacting database...") + try: + await self.database.vacuum() + except Exception as err: + self.logger.warning("Database vacuum failed: %s", str(err)) + else: + self.logger.debug("Compacting database done") + + async def __migrate_database(self, prev_version: int) -> None: + """Perform a database migration.""" + # ruff: noqa: PLR0915 + self.logger.info( + "Migrating database from version %s to %s", prev_version, DB_SCHEMA_VERSION + ) + + if prev_version <= 4: + # unhandled schema version + # we do not try to handle more complex migrations + self.logger.warning( + "Database schema too old - Resetting library/database - " + "a full rescan will be performed, this can take a while!" + ) + for table in ( + DB_TABLE_TRACKS, + DB_TABLE_ALBUMS, + DB_TABLE_ARTISTS, + DB_TABLE_PLAYLISTS, + DB_TABLE_RADIOS, + DB_TABLE_ALBUM_TRACKS, + DB_TABLE_PLAYLOG, + DB_TABLE_PROVIDER_MAPPINGS, + ): + await self.database.execute(f"DROP TABLE IF EXISTS {table}") + await self.database.commit() + # recreate missing tables + await self.__create_database_tables() + return + + if prev_version <= 7: + # remove redundant artists and provider_mappings columns + for table in ( + DB_TABLE_TRACKS, + DB_TABLE_ALBUMS, + DB_TABLE_ARTISTS, + DB_TABLE_RADIOS, + DB_TABLE_PLAYLISTS, + ): + for column in ("artists", "provider_mappings"): + try: + await self.database.execute(f"ALTER TABLE {table} DROP COLUMN {column}") + except Exception as err: + if "no such column" in str(err): + continue + raise + # add cache_checksum column to playlists + try: + await self.database.execute( + f"ALTER TABLE {DB_TABLE_PLAYLISTS} ADD COLUMN cache_checksum TEXT DEFAULT ''" + ) + except Exception as err: + if "duplicate column" not in str(err): + raise + + if prev_version <= 8: + # migrate track_loudness --> loudness_measurements + async for db_row in self.database.iter_items("track_loudness"): + if db_row["integrated"] == inf or db_row["integrated"] == -inf: + continue + if db_row["provider"] in ("radiobrowser", "tunein"): + continue + await self.database.insert_or_replace( + DB_TABLE_LOUDNESS_MEASUREMENTS, + { + "item_id": db_row["item_id"], + "media_type": "track", + "provider": db_row["provider"], + "loudness": db_row["integrated"], + }, + ) + await self.database.execute("DROP TABLE IF EXISTS track_loudness") + + # save changes + await self.database.commit() + + # always clear the cache after a db migration + await self.mass.cache.clear() + + async def _reset_database(self) -> None: + """Reset the database.""" + await self.close() + db_path = os.path.join(self.mass.storage_path, "library.db") + await asyncio.to_thread(os.remove, db_path) + await self._setup_database() + + async def __create_database_tables(self) -> None: + """Create database tables.""" + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_SETTINGS}( + [key] TEXT PRIMARY KEY, + [value] TEXT, + [type] TEXT + );""" + ) + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_PLAYLOG}( + [id] INTEGER PRIMARY KEY AUTOINCREMENT, + [item_id] TEXT NOT NULL, + [provider] TEXT NOT NULL, + [media_type] TEXT NOT NULL DEFAULT 'track', + [timestamp] INTEGER DEFAULT 0, + UNIQUE(item_id, provider, media_type));""" + ) + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUMS}( + [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, + [name] TEXT NOT NULL, + [sort_name] TEXT NOT NULL, + [version] TEXT, + [album_type] TEXT NOT NULL, + [year] INTEGER, + [favorite] BOOLEAN DEFAULT 0, + [metadata] json NOT NULL, + [external_ids] json NOT NULL, + [play_count] INTEGER DEFAULT 0, + [last_played] INTEGER DEFAULT 0, + [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), + [timestamp_modified] INTEGER + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_ARTISTS}( + [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, + [name] TEXT NOT NULL, + [sort_name] TEXT NOT NULL, + [favorite] BOOLEAN DEFAULT 0, + [metadata] json NOT NULL, + [external_ids] json NOT NULL, + [play_count] INTEGER DEFAULT 0, + [last_played] INTEGER DEFAULT 0, + [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), + [timestamp_modified] INTEGER + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_TRACKS}( + [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, + [name] TEXT NOT NULL, + [sort_name] TEXT NOT NULL, + [version] TEXT, + [duration] INTEGER, + [favorite] BOOLEAN DEFAULT 0, + [metadata] json NOT NULL, + [external_ids] json NOT NULL, + [play_count] INTEGER DEFAULT 0, + [last_played] INTEGER DEFAULT 0, + [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), + [timestamp_modified] INTEGER + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_PLAYLISTS}( + [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, + [name] TEXT NOT NULL, + [sort_name] TEXT NOT NULL, + [owner] TEXT NOT NULL, + [is_editable] BOOLEAN NOT NULL, + [cache_checksum] TEXT DEFAULT '', + [favorite] BOOLEAN DEFAULT 0, + [metadata] json NOT NULL, + [external_ids] json NOT NULL, + [play_count] INTEGER DEFAULT 0, + [last_played] INTEGER DEFAULT 0, + [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), + [timestamp_modified] INTEGER + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_RADIOS}( + [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, + [name] TEXT NOT NULL, + [sort_name] TEXT NOT NULL, + [favorite] BOOLEAN DEFAULT 0, + [metadata] json NOT NULL, + [external_ids] json NOT NULL, + [play_count] INTEGER DEFAULT 0, + [last_played] INTEGER DEFAULT 0, + [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), + [timestamp_modified] INTEGER + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUM_TRACKS}( + [id] INTEGER PRIMARY KEY AUTOINCREMENT, + [track_id] INTEGER NOT NULL, + [album_id] INTEGER NOT NULL, + [disc_number] INTEGER NOT NULL, + [track_number] INTEGER NOT NULL, + FOREIGN KEY([track_id]) REFERENCES [tracks]([item_id]), + FOREIGN KEY([album_id]) REFERENCES [albums]([item_id]), + UNIQUE(track_id, album_id) + );""" + ) + await self.database.execute( + f""" + CREATE TABLE IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}( + [media_type] TEXT NOT NULL, + [item_id] INTEGER NOT NULL, + [provider_domain] TEXT NOT NULL, + [provider_instance] TEXT NOT NULL, + [provider_item_id] TEXT NOT NULL, + [available] BOOLEAN DEFAULT 1, + [url] text, + [audio_format] json, + [details] TEXT, + UNIQUE(media_type, provider_instance, provider_item_id) + );""" + ) + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}( + [track_id] INTEGER NOT NULL, + [artist_id] INTEGER NOT NULL, + FOREIGN KEY([track_id]) REFERENCES [tracks]([item_id]), + FOREIGN KEY([artist_id]) REFERENCES [artists]([item_id]), + UNIQUE(track_id, artist_id) + );""" + ) + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}( + [album_id] INTEGER NOT NULL, + [artist_id] INTEGER NOT NULL, + FOREIGN KEY([album_id]) REFERENCES [albums]([item_id]), + FOREIGN KEY([artist_id]) REFERENCES [artists]([item_id]), + UNIQUE(album_id, artist_id) + );""" + ) + + await self.database.execute( + f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_LOUDNESS_MEASUREMENTS}( + [id] INTEGER PRIMARY KEY AUTOINCREMENT, + [media_type] TEXT NOT NULL, + [item_id] TEXT NOT NULL, + [provider] TEXT NOT NULL, + [loudness] REAL, + [loudness_album] REAL, + UNIQUE(media_type,item_id,provider));""" + ) + + await self.database.commit() + + async def __create_database_indexes(self) -> None: + """Create database indexes.""" + for db_table in ( + DB_TABLE_ARTISTS, + DB_TABLE_ALBUMS, + DB_TABLE_TRACKS, + DB_TABLE_PLAYLISTS, + DB_TABLE_RADIOS, + ): + # index on favorite column + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_favorite_idx on {db_table}(favorite);" + ) + # index on name + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_name_idx on {db_table}(name);" + ) + # index on name (without case sensitivity) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_name_nocase_idx " + f"ON {db_table}(name COLLATE NOCASE);" + ) + # index on sort_name + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_sort_name_idx on {db_table}(sort_name);" + ) + # index on sort_name (without case sensitivity) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_sort_name_nocase_idx " + f"ON {db_table}(sort_name COLLATE NOCASE);" + ) + # index on external_ids + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_external_ids_idx " + f"ON {db_table}(external_ids);" + ) + # index on timestamp_added + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_timestamp_added_idx " + f"on {db_table}(timestamp_added);" + ) + # index on play_count + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_play_count_idx " + f"on {db_table}(play_count);" + ) + # index on last_played + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {db_table}_last_played_idx " + f"on {db_table}(last_played);" + ) + + # indexes on provider_mappings table + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_media_type_item_id_idx " + f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,item_id);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_provider_domain_idx " + f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_domain,provider_item_id);" + ) + await self.database.execute( + f"CREATE UNIQUE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_provider_instance_idx " + f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_instance,provider_item_id);" + ) + await self.database.execute( + "CREATE INDEX IF NOT EXISTS " + f"{DB_TABLE_PROVIDER_MAPPINGS}_media_type_provider_instance_idx " + f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_instance);" + ) + await self.database.execute( + "CREATE INDEX IF NOT EXISTS " + f"{DB_TABLE_PROVIDER_MAPPINGS}_media_type_provider_domain_idx " + f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_domain);" + ) + + # indexes on track_artists table + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}_track_id_idx " + f"on {DB_TABLE_TRACK_ARTISTS}(track_id);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}_artist_id_idx " + f"on {DB_TABLE_TRACK_ARTISTS}(artist_id);" + ) + # indexes on album_artists table + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}_album_id_idx " + f"on {DB_TABLE_ALBUM_ARTISTS}(album_id);" + ) + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}_artist_id_idx " + f"on {DB_TABLE_ALBUM_ARTISTS}(artist_id);" + ) + # index on loudness measurements table + await self.database.execute( + f"CREATE INDEX IF NOT EXISTS {DB_TABLE_LOUDNESS_MEASUREMENTS}_idx " + f"on {DB_TABLE_LOUDNESS_MEASUREMENTS}(media_type,item_id,provider);" + ) + await self.database.commit() + + async def __create_database_triggers(self) -> None: + """Create database triggers.""" + # triggers to auto update timestamps + for db_table in ("artists", "albums", "tracks", "playlists", "radios"): + await self.database.execute( + f""" + CREATE TRIGGER IF NOT EXISTS update_{db_table}_timestamp + AFTER UPDATE ON {db_table} FOR EACH ROW + WHEN NEW.timestamp_modified <= OLD.timestamp_modified + BEGIN + UPDATE {db_table} set timestamp_modified=cast(strftime('%s','now') as int) + WHERE item_id=OLD.item_id; + END; + """ + ) + await self.database.commit() diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py new file mode 100644 index 00000000..f605d69e --- /dev/null +++ b/music_assistant/controllers/player_queues.py @@ -0,0 +1,1550 @@ +""" +MusicAssistant Player Queues Controller. + +Handles all logic to PLAY Media Items, provided by Music Providers to supported players. + +It is loosely coupled to the MusicAssistant Music Controller and Player Controller. +A Music Assistant Player always has a PlayerQueue associated with it +which holds the queue items and state. + +The PlayerQueue is in that case the active source of the player, +but it can also be something else, hence the loose coupling. +""" + +from __future__ import annotations + +import asyncio +import random +import time +from typing import TYPE_CHECKING, Any, TypedDict + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( + CacheCategory, + ConfigEntryType, + EventType, + MediaType, + PlayerState, + ProviderFeature, + QueueOption, + RepeatMode, +) +from music_assistant_models.errors import ( + InvalidCommand, + MediaNotFoundError, + MusicAssistantError, + PlayerUnavailableError, + QueueEmpty, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import AudioFormat, MediaItemType, Playlist, media_from_dict +from music_assistant_models.player import PlayerMedia +from music_assistant_models.player_queue import PlayerQueue +from music_assistant_models.queue_item import QueueItem +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import CONF_CROSSFADE, CONF_FLOW_MODE, MASS_LOGO_ONLINE +from music_assistant.helpers.api import api_command +from music_assistant.helpers.audio import get_stream_details +from music_assistant.helpers.throttle_retry import BYPASS_THROTTLER +from music_assistant.helpers.util import get_changed_keys +from music_assistant.models.core_controller import CoreController + +if TYPE_CHECKING: + from collections.abc import Iterator + + from music_assistant_models.media_items import Album, Artist, Track + from music_assistant_models.player import Player + + +CONF_DEFAULT_ENQUEUE_SELECT_ARTIST = "default_enqueue_select_artist" +CONF_DEFAULT_ENQUEUE_SELECT_ALBUM = "default_enqueue_select_album" + +ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE = "all_tracks" +ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE = "all_tracks" + +CONF_DEFAULT_ENQUEUE_OPTION_ARTIST = "default_enqueue_option_artist" +CONF_DEFAULT_ENQUEUE_OPTION_ALBUM = "default_enqueue_option_album" +CONF_DEFAULT_ENQUEUE_OPTION_TRACK = "default_enqueue_option_track" +CONF_DEFAULT_ENQUEUE_OPTION_RADIO = "default_enqueue_option_radio" +CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST = "default_enqueue_option_playlist" +RADIO_TRACK_MAX_DURATION_SECS = 20 * 60 # 20 minutes + + +class CompareState(TypedDict): + """Simple object where we store the (previous) state of a queue. + + Used for compare actions. + """ + + queue_id: str + state: PlayerState + current_index: int | None + elapsed_time: int + stream_title: str | None + content_type: str | None + + +class PlayerQueuesController(CoreController): + """Controller holding all logic to enqueue music for players.""" + + domain: str = "player_queues" + + def __init__(self, *args, **kwargs) -> None: + """Initialize core controller.""" + super().__init__(*args, **kwargs) + self._queues: dict[str, PlayerQueue] = {} + self._queue_items: dict[str, list[QueueItem]] = {} + self._prev_states: dict[str, CompareState] = {} + self.manifest.name = "Player Queues controller" + self.manifest.description = ( + "Music Assistant's core controller which manages the queues for all players." + ) + self.manifest.icon = "playlist-music" + + async def close(self) -> None: + """Cleanup on exit.""" + # stop all playback + for queue in self.all(): + if queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): + continue + await self.stop(queue.queue_id) + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + enqueue_options = tuple(ConfigValueOption(x.name, x.value) for x in QueueOption) + return ( + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, + type=ConfigEntryType.STRING, + default_value=ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, + label="Items to select when you play a (in-library) artist.", + options=( + ConfigValueOption( + title="Only in-library tracks", + value="library_tracks", + ), + ConfigValueOption( + title="All tracks from all albums in the library", + value="library_album_tracks", + ), + ConfigValueOption( + title="All (top) tracks from (all) streaming provider(s)", + value="all_tracks", + ), + ConfigValueOption( + title="All tracks from all albums from (all) streaming provider(s)", + value="all_album_tracks", + ), + ), + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, + type=ConfigEntryType.STRING, + default_value=ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, + label="Items to select when you play a (in-library) album.", + options=( + ConfigValueOption( + title="Only in-library tracks", + value="library_tracks", + ), + ConfigValueOption( + title="All tracks for album on (streaming) provider", + value="all_tracks", + ), + ), + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_ARTIST, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE.value, + label="Default enqueue option for Artist item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_ALBUM, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE.value, + label="Default enqueue option for Album item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_TRACK, + type=ConfigEntryType.STRING, + default_value=QueueOption.PLAY.value, + label="Default enqueue option for Track item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_RADIO, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE.value, + label="Default enqueue option for Radio item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE.value, + label="Default enqueue option for Playlist item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ) + + def __iter__(self) -> Iterator[PlayerQueue]: + """Iterate over (available) players.""" + return iter(self._queues.values()) + + @api_command("player_queues/all") + def all(self) -> tuple[PlayerQueue, ...]: + """Return all registered PlayerQueues.""" + return tuple(self._queues.values()) + + @api_command("player_queues/get") + def get(self, queue_id: str) -> PlayerQueue | None: + """Return PlayerQueue by queue_id or None if not found.""" + return self._queues.get(queue_id) + + @api_command("player_queues/items") + def items(self, queue_id: str, limit: int = 500, offset: int = 0) -> list[QueueItem]: + """Return all QueueItems for given PlayerQueue.""" + if queue_id not in self._queue_items: + return [] + + return self._queue_items[queue_id][offset : offset + limit] + + @api_command("player_queues/get_active_queue") + def get_active_queue(self, player_id: str) -> PlayerQueue: + """Return the current active/synced queue for a player.""" + if player := self.mass.players.get(player_id): + # account for player that is synced (sync child) + if player.synced_to and player.synced_to != player.player_id: + return self.get_active_queue(player.synced_to) + # handle active group player + if player.active_group and player.active_group != player.player_id: + return self.get_active_queue(player.active_group) + # active_source may be filled with other queue id + return self.get(player.active_source) or self.get(player_id) + return self.get(player_id) + + # Queue commands + + @api_command("player_queues/shuffle") + def set_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None: + """Configure shuffle setting on the the queue.""" + queue = self._queues[queue_id] + if queue.shuffle_enabled == shuffle_enabled: + return # no change + queue.shuffle_enabled = shuffle_enabled + queue_items = self._queue_items[queue_id] + cur_index = queue.index_in_buffer or queue.current_index + if cur_index is not None: + next_index = cur_index + 1 + next_items = queue_items[next_index:] + else: + next_items = [] + next_index = 0 + if not shuffle_enabled: + # shuffle disabled, try to restore original sort order of the remaining items + next_items.sort(key=lambda x: x.sort_index, reverse=False) + self.load( + queue_id=queue_id, + queue_items=next_items, + insert_at_index=next_index, + keep_remaining=False, + shuffle=shuffle_enabled, + ) + + @api_command("player_queues/dont_stop_the_music") + def set_dont_stop_the_music(self, queue_id: str, dont_stop_the_music_enabled: bool) -> None: + """Configure Don't stop the music setting on the queue.""" + providers_available_with_similar_tracks = any( + ProviderFeature.SIMILAR_TRACKS in provider.supported_features + for provider in self.mass.music.providers + ) + if dont_stop_the_music_enabled and not providers_available_with_similar_tracks: + raise UnsupportedFeaturedException( + "Don't stop the music is not supported by any of the available music providers" + ) + queue = self._queues[queue_id] + queue.dont_stop_the_music_enabled = dont_stop_the_music_enabled + self.signal_update(queue_id=queue_id) + # if this happens to be the last track in the queue, fill the radio source + if ( + queue.dont_stop_the_music_enabled + and queue.enqueued_media_items + and queue.current_index is not None + and (queue.items - queue.current_index) <= 1 + ): + queue.radio_source = queue.enqueued_media_items + task_id = f"fill_radio_tracks_{queue_id}" + self.mass.call_later(5, self._fill_radio_tracks, queue_id, task_id=task_id) + + @api_command("player_queues/repeat") + def set_repeat(self, queue_id: str, repeat_mode: RepeatMode) -> None: + """Configure repeat setting on the the queue.""" + queue = self._queues[queue_id] + if queue.repeat_mode == repeat_mode: + return # no change + queue.repeat_mode = repeat_mode + self.signal_update(queue_id) + + @api_command("player_queues/play_media") + async def play_media( + self, + queue_id: str, + media: MediaItemType | list[MediaItemType] | str | list[str], + option: QueueOption | None = None, + radio_mode: bool = False, + start_item: str | None = None, + ) -> None: + """Play media item(s) on the given queue. + + - media: Media that should be played (MediaItem(s) or uri's). + - queue_opt: Which enqueue mode to use. + - radio_mode: Enable radio mode for the given item(s). + - start_item: Optional item to start the playlist or album from. + """ + # ruff: noqa: PLR0915,PLR0912 + # we use a contextvar to bypass the throttler for this asyncio task/context + # this makes sure that playback has priority over other requests that may be + # happening in the background + BYPASS_THROTTLER.set(True) + queue = self._queues[queue_id] + # always fetch the underlying player so we can raise early if its not available + queue_player = self.mass.players.get(queue_id, True) + if queue_player.announcement_in_progress: + self.logger.warning("Ignore queue command: An announcement is in progress") + return + + # a single item or list of items may be provided + if not isinstance(media, list): + media = [media] + + # clear queue first if it was finished + if queue.current_index and queue.current_index >= (len(self._queue_items[queue_id]) - 1): + queue.current_index = None + self._queue_items[queue_id] = [] + # clear queue if needed + if option == QueueOption.REPLACE: + self.clear(queue_id) + # Clear the 'enqueued media item' list when a new queue is requested + if option not in (QueueOption.ADD, QueueOption.NEXT): + queue.enqueued_media_items.clear() + + tracks: list[MediaItemType] = [] + radio_source: list[MediaItemType] = [] + first_track_seen: bool = False + for item in media: + try: + # parse provided uri into a MA MediaItem or Basic QueueItem from URL + if isinstance(item, str): + media_item = await self.mass.music.get_item_by_uri(item) + elif isinstance(item, dict): + media_item = media_from_dict(item) + else: + media_item = item + + # Save requested media item to play on the queue so we can use it as a source + # for Don't stop the music. Use FIFO list to keep track of the last 10 played items + if media_item.media_type in ( + MediaType.TRACK, + MediaType.ALBUM, + MediaType.PLAYLIST, + MediaType.ARTIST, + ): + queue.enqueued_media_items.append(media_item) + if len(queue.enqueued_media_items) > 10: + queue.enqueued_media_items.pop(0) + + # handle default enqueue option if needed + if option is None: + option = QueueOption( + await self.mass.config.get_core_config_value( + self.domain, + f"default_enqueue_option_{media_item.media_type.value}", + ) + ) + if option == QueueOption.REPLACE: + self.clear(queue_id) + + # collect tracks to play + if radio_mode: + radio_source.append(media_item) + elif media_item.media_type == MediaType.PLAYLIST: + tracks += await self.get_playlist_tracks(media_item, start_item) + self.mass.create_task( + self.mass.music.mark_item_played( + media_item.media_type, media_item.item_id, media_item.provider + ) + ) + elif media_item.media_type == MediaType.ARTIST: + tracks += await self.get_artist_tracks(media_item) + self.mass.create_task( + self.mass.music.mark_item_played( + media_item.media_type, media_item.item_id, media_item.provider + ) + ) + elif media_item.media_type == MediaType.ALBUM: + tracks += await self.get_album_tracks(media_item, start_item) + self.mass.create_task( + self.mass.music.mark_item_played( + media_item.media_type, media_item.item_id, media_item.provider + ) + ) + else: + # single track or radio item + tracks += [media_item] + + except MusicAssistantError as err: + # invalid MA uri or item not found error + self.logger.warning("Skipping %s: %s", item, str(err)) + + # overwrite or append radio source items + if option not in (QueueOption.ADD, QueueOption.NEXT): + queue.radio_source = radio_source + else: + queue.radio_source += radio_source + # Use collected media items to calculate the radio if radio mode is on + if radio_mode: + tracks = await self._get_radio_tracks(queue_id=queue_id, is_initial_radio_mode=True) + + # only add valid/available items + queue_items = [QueueItem.from_media_item(queue_id, x) for x in tracks if x and x.available] + + if not queue_items: + if first_track_seen: + # edge case: playlist with only one track + return + raise MediaNotFoundError("No playable items found") + + # load the items into the queue + if queue.state in (PlayerState.PLAYING, PlayerState.PAUSED): + cur_index = queue.index_in_buffer or 0 + else: + cur_index = queue.current_index or 0 + insert_at_index = cur_index + 1 if self._queue_items.get(queue_id) else 0 + # Radio modes are already shuffled in a pattern we would like to keep. + shuffle = queue.shuffle_enabled and len(queue_items) > 1 and not radio_mode + + # handle replace: clear all items and replace with the new items + if option == QueueOption.REPLACE: + self.load( + queue_id, + queue_items=queue_items, + keep_remaining=False, + keep_played=False, + shuffle=shuffle, + ) + await self.play_index(queue_id, 0) + return + # handle next: add item(s) in the index next to the playing/loaded/buffered index + if option == QueueOption.NEXT: + self.load( + queue_id, + queue_items=queue_items, + insert_at_index=insert_at_index, + shuffle=shuffle, + ) + return + if option == QueueOption.REPLACE_NEXT: + self.load( + queue_id, + queue_items=queue_items, + insert_at_index=insert_at_index, + keep_remaining=False, + shuffle=shuffle, + ) + return + # handle play: replace current loaded/playing index with new item(s) + if option == QueueOption.PLAY: + self.load( + queue_id, + queue_items=queue_items, + insert_at_index=insert_at_index, + shuffle=shuffle, + ) + next_index = min(insert_at_index, len(self._queue_items[queue_id]) - 1) + await self.play_index(queue_id, next_index) + return + # handle add: add/append item(s) to the remaining queue items + if option == QueueOption.ADD: + self.load( + queue_id=queue_id, + queue_items=queue_items, + insert_at_index=insert_at_index + if queue.shuffle_enabled + else len(self._queue_items[queue_id]), + shuffle=queue.shuffle_enabled, + ) + # handle edgecase, queue is empty and items are only added (not played) + # mark first item as new index + if queue.current_index is None: + queue.current_index = 0 + queue.current_item = self.get_item(queue_id, 0) + queue.items = len(queue_items) + self.signal_update(queue_id) + + @api_command("player_queues/move_item") + def move_item(self, queue_id: str, queue_item_id: str, pos_shift: int = 1) -> None: + """ + Move queue item x up/down the queue. + + - queue_id: id of the queue to process this request. + - queue_item_id: the item_id of the queueitem that needs to be moved. + - pos_shift: move item x positions down if positive value + - pos_shift: move item x positions up if negative value + - pos_shift: move item to top of queue as next item if 0. + """ + queue = self._queues[queue_id] + item_index = self.index_by_id(queue_id, queue_item_id) + if item_index <= queue.index_in_buffer: + msg = f"{item_index} is already played/buffered" + raise IndexError(msg) + + queue_items = self._queue_items[queue_id] + queue_items = queue_items.copy() + + if pos_shift == 0 and queue.state == PlayerState.PLAYING: + new_index = (queue.current_index or 0) + 1 + elif pos_shift == 0: + new_index = queue.current_index or 0 + else: + new_index = item_index + pos_shift + if (new_index < (queue.current_index or 0)) or (new_index > len(queue_items)): + return + # move the item in the list + queue_items.insert(new_index, queue_items.pop(item_index)) + self.update_items(queue_id, queue_items) + + @api_command("player_queues/delete_item") + def delete_item(self, queue_id: str, item_id_or_index: int | str) -> None: + """Delete item (by id or index) from the queue.""" + if isinstance(item_id_or_index, str): + item_index = self.index_by_id(queue_id, item_id_or_index) + else: + item_index = item_id_or_index + queue = self._queues[queue_id] + if item_index <= queue.index_in_buffer: + # ignore request if track already loaded in the buffer + # the frontend should guard so this is just in case + self.logger.warning("delete requested for item already loaded in buffer") + return + queue_items = self._queue_items[queue_id] + queue_items.pop(item_index) + self.update_items(queue_id, queue_items) + + @api_command("player_queues/clear") + def clear(self, queue_id: str) -> None: + """Clear all items in the queue.""" + queue = self._queues[queue_id] + queue.radio_source = [] + if queue.state != PlayerState.IDLE: + self.mass.create_task(self.stop(queue_id)) + queue.current_index = None + queue.current_item = None + queue.elapsed_time = 0 + queue.index_in_buffer = None + self.update_items(queue_id, []) + + @api_command("player_queues/stop") + async def stop(self, queue_id: str) -> None: + """ + Handle STOP command for given queue. + + - queue_id: queue_id of the playerqueue to handle the command. + """ + if (queue := self.get(queue_id)) and queue.active: + queue.resume_pos = queue.corrected_elapsed_time + # forward the actual command to the player provider + if player_provider := self.mass.players.get_player_provider(queue.queue_id): + await player_provider.cmd_stop(queue_id) + + @api_command("player_queues/play") + async def play(self, queue_id: str) -> None: + """ + Handle PLAY command for given queue. + + - queue_id: queue_id of the playerqueue to handle the command. + """ + queue_player: Player = self.mass.players.get(queue_id, True) + if ( + (queue := self._queues.get(queue_id)) + and queue.active + and queue_player.state == PlayerState.PAUSED + ): + # forward the actual play/unpause command to the player provider + if player_provider := self.mass.players.get_player_provider(queue.queue_id): + await player_provider.cmd_play(queue_id) + return + # player is not paused, perform resume instead + await self.resume(queue_id) + + @api_command("player_queues/pause") + async def pause(self, queue_id: str) -> None: + """Handle PAUSE command for given queue. + + - queue_id: queue_id of the playerqueue to handle the command. + """ + if queue := self._queues.get(queue_id): + queue.resume_pos = queue.corrected_elapsed_time + # forward the actual command to the player controller + await self.mass.players.cmd_pause(queue_id) + + @api_command("player_queues/play_pause") + async def play_pause(self, queue_id: str) -> None: + """Toggle play/pause on given playerqueue. + + - queue_id: queue_id of the queue to handle the command. + """ + if (queue := self._queues.get(queue_id)) and queue.state == PlayerState.PLAYING: + await self.pause(queue_id) + return + await self.play(queue_id) + + @api_command("player_queues/next") + async def next(self, queue_id: str) -> None: + """Handle NEXT TRACK command for given queue. + + - queue_id: queue_id of the queue to handle the command. + """ + if (queue := self.get(queue_id)) is None or not queue.active: + # TODO: forward to underlying player if not active + return + idx = self._queues[queue_id].current_index + while True: + try: + if (next_index := self._get_next_index(queue_id, idx, True)) is not None: + await self.play_index(queue_id, next_index, debounce=True) + break + except MediaNotFoundError: + self.logger.warning( + "Failed to fetch next track for queue %s - trying next item", queue.display_name + ) + idx += 1 + + @api_command("player_queues/previous") + async def previous(self, queue_id: str) -> None: + """Handle PREVIOUS TRACK command for given queue. + + - queue_id: queue_id of the queue to handle the command. + """ + if (queue := self.get(queue_id)) is None or not queue.active: + # TODO: forward to underlying player if not active + return + current_index = self._queues[queue_id].current_index + if current_index is None: + return + await self.play_index(queue_id, max(current_index - 1, 0), debounce=True) + + @api_command("player_queues/skip") + async def skip(self, queue_id: str, seconds: int = 10) -> None: + """Handle SKIP command for given queue. + + - queue_id: queue_id of the queue to handle the command. + - seconds: number of seconds to skip in track. Use negative value to skip back. + """ + if (queue := self.get(queue_id)) is None or not queue.active: + # TODO: forward to underlying player if not active + return + await self.seek(queue_id, self._queues[queue_id].elapsed_time + seconds) + + @api_command("player_queues/seek") + async def seek(self, queue_id: str, position: int = 10) -> None: + """Handle SEEK command for given queue. + + - queue_id: queue_id of the queue to handle the command. + - position: position in seconds to seek to in the current playing item. + """ + if not (queue := self.get(queue_id)): + return + queue_player: Player = self.mass.players.get(queue_id, True) + if not queue.current_item: + raise InvalidCommand(f"Queue {queue_player.display_name} has no item(s) loaded.") + if ( + queue.current_item.media_item.media_type != MediaType.TRACK + or not queue.current_item.duration + ): + raise InvalidCommand("Can not seek on non track items.") + position = max(0, int(position)) + if position > queue.current_item.duration: + raise InvalidCommand("Can not seek outside of duration range.") + await self.play_index(queue_id, queue.current_index, seek_position=position) + + @api_command("player_queues/resume") + async def resume(self, queue_id: str, fade_in: bool | None = None) -> None: + """Handle RESUME command for given queue. + + - queue_id: queue_id of the queue to handle the command. + """ + queue = self._queues[queue_id] + queue_items = self._queue_items[queue_id] + resume_item = queue.current_item + if queue.state == PlayerState.PLAYING: + # resume requested while already playing, + # use current position as resume position + resume_pos = queue.corrected_elapsed_time + else: + resume_pos = queue.resume_pos + + if not resume_item and queue.current_index is not None and len(queue_items) > 0: + resume_item = self.get_item(queue_id, queue.current_index) + resume_pos = 0 + elif not resume_item and queue.current_index is None and len(queue_items) > 0: + # items available in queue but no previous track, start at 0 + resume_item = self.get_item(queue_id, 0) + 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 not queue_player.powered: + 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 + resume_pos = 0 + await self.play_index(queue_id, resume_item.queue_item_id, resume_pos, fade_in) + else: + msg = f"Resume queue requested but queue {queue.display_name} is empty" + raise QueueEmpty(msg) + + @api_command("player_queues/play_index") + async def play_index( + self, + queue_id: str, + index: int | str, + seek_position: int = 0, + fade_in: bool = False, + debounce: bool = False, + ) -> None: + """Play item at index (or item_id) X in queue.""" + queue = self._queues[queue_id] + queue.resume_pos = 0 + if isinstance(index, str): + index = self.index_by_id(queue_id, index) + queue_item = self.get_item(queue_id, index) + if queue_item is None: + msg = f"Unknown index/id: {index}" + raise FileNotFoundError(msg) + queue.current_index = index + queue.index_in_buffer = index + queue.flow_mode_stream_log = [] + queue.flow_mode = await self.mass.config.get_player_config_value(queue_id, CONF_FLOW_MODE) + next_index = self._get_next_index(queue_id, index, allow_repeat=False) + queue.current_item = queue_item + queue.next_track_enqueued = None + self.signal_update(queue_id) + + # work out if we are playing an album and if we should prefer album loudness + if ( + next_index is not None + and (next_item := self.get_item(queue_id, next_index)) + and ( + queue_item.media_item + and hasattr(queue_item.media_item, "album") + and hasattr(next_item.media_item, "album") + and queue_item.media_item.album + and next_item.media_item + and next_item.media_item.album + and queue_item.media_item.album.item_id == next_item.media_item.album.item_id + ) + ): + prefer_album_loudness = True + else: + prefer_album_loudness = False + + # get streamdetails - do this here to catch unavailable items early + queue_item.streamdetails = await get_stream_details( + self.mass, + queue_item, + seek_position=seek_position, + fade_in=fade_in, + prefer_album_loudness=prefer_album_loudness, + ) + + # allow stripping silence from the end of the track if crossfade is enabled + # this will allow for smoother crossfades + if await self.mass.config.get_player_config_value(queue_id, CONF_CROSSFADE): + queue_item.streamdetails.strip_silence_end = True + # send play_media request to player + # NOTE that we debounce this a bit to account for someone hitting the next button + # like a madman. This will prevent the player from being overloaded with requests. + self.mass.call_later( + 1 if debounce else 0.1, + self.mass.players.play_media, + player_id=queue_id, + # transform into PlayerMedia to send to the actual player implementation + media=self.player_media_from_queue_item(queue_item, queue.flow_mode), + task_id=f"play_media_{queue_id}", + ) + self.signal_update(queue_id) + + @api_command("player_queues/transfer") + async def transfer_queue( + self, + source_queue_id: str, + target_queue_id: str, + auto_play: bool | None = None, + ) -> None: + """Transfer queue to another queue.""" + if not (source_queue := self.get(source_queue_id)): + raise PlayerUnavailableError("Queue {source_queue_id} is not available") + if not (target_queue := self.get(target_queue_id)): + raise PlayerUnavailableError("Queue {target_queue_id} is not available") + if auto_play is None: + auto_play = source_queue.state == PlayerState.PLAYING + + target_player = self.mass.players.get(target_queue_id) + if target_player.active_group or target_player.synced_to: + # edge case: the user wants to move playback from the group as a whole, to a single + # player in the group or it is grouped and the command targeted at the single player. + # We need to dissolve the group first. + await self.mass.players.cmd_power( + target_player.active_group or target_player.synced_to, False + ) + await asyncio.sleep(3) + + source_items = self._queue_items[source_queue_id] + target_queue.repeat_mode = source_queue.repeat_mode + target_queue.shuffle_enabled = source_queue.shuffle_enabled + target_queue.dont_stop_the_music_enabled = source_queue.dont_stop_the_music_enabled + target_queue.radio_source = source_queue.radio_source + target_queue.resume_pos = source_queue.elapsed_time + target_queue.current_index = source_queue.current_index + if source_queue.current_item: + target_queue.current_item = source_queue.current_item + target_queue.current_item.queue_id = target_queue_id + self.clear(source_queue_id) + + self.load(target_queue_id, source_items, keep_remaining=False, keep_played=False) + for item in source_items: + item.queue_id = target_queue_id + self.update_items(target_queue_id, source_items) + if auto_play: + await self.resume(target_queue_id) + + # Interaction with player + + async def on_player_register(self, player: Player) -> None: + """Register PlayerQueue for given player/queue id.""" + queue_id = player.player_id + queue = None + # try to restore previous state + if prev_state := await self.mass.cache.get( + "state", category=CacheCategory.PLAYER_QUEUE_STATE, base_key=queue_id + ): + try: + queue = PlayerQueue.from_cache(prev_state) + prev_items = await self.mass.cache.get( + "items", + default=[], + category=CacheCategory.PLAYER_QUEUE_STATE, + base_key=queue_id, + ) + queue_items = [QueueItem.from_cache(x) for x in prev_items] + except Exception as err: + self.logger.warning( + "Failed to restore the queue(items) for %s - %s", + player.display_name, + str(err), + ) + if queue is None: + queue = PlayerQueue( + queue_id=queue_id, + active=False, + display_name=player.display_name, + available=player.available, + dont_stop_the_music_enabled=False, + items=0, + ) + queue_items = [] + + self._queues[queue_id] = queue + self._queue_items[queue_id] = queue_items + # always call update to calculate state etc + self.on_player_update(player, {}) + self.mass.signal_event(EventType.QUEUE_ADDED, object_id=queue_id, data=queue) + + def on_player_update( + self, + player: Player, + changed_values: dict[str, tuple[Any, Any]], + ) -> None: + """ + Call when a PlayerQueue needs to be updated (e.g. when player updates). + + NOTE: This is called every second if the player is playing. + """ + if player.player_id not in self._queues: + # race condition + return + if player.announcement_in_progress: + # do nothing while the announcement is in progress + return + queue_id = player.player_id + player = self.mass.players.get(queue_id) + queue = self._queues[queue_id] + + # basic properties + queue.display_name = player.display_name + queue.available = player.available + queue.items = len(self._queue_items[queue_id]) + # determine if this queue is currently active for this player + queue.active = player.powered and player.active_source == queue.queue_id + if not queue.active: + # return early if the queue is not active + queue.state = PlayerState.IDLE + if prev_state := self._prev_states.pop(queue_id, None): + self.signal_update(queue_id) + return + # update current item from player report + if queue.flow_mode: + # flow mode active, calculate current item + queue.current_index, queue.elapsed_time = self._get_flow_queue_stream_index( + queue, player + ) + queue.elapsed_time_last_updated = time.time() + else: + # queue is active and player has one of our tracks loaded, update state + if item_id := self._parse_player_current_item_id(queue_id, player): + queue.current_index = self.index_by_id(queue_id, item_id) + if player.state in (PlayerState.PLAYING, PlayerState.PAUSED): + queue.elapsed_time = int(player.corrected_elapsed_time or 0) + queue.elapsed_time_last_updated = player.elapsed_time_last_updated or 0 + + # only update these attributes if the queue is active + # and has an item loaded so we are able to resume it + queue.state = player.state or PlayerState.IDLE + queue.current_item = self.get_item(queue_id, queue.current_index) + queue.next_item = ( + self.get_item(queue_id, queue.next_track_enqueued) + if queue.next_track_enqueued + else self._get_next_item(queue_id, queue.current_index) + ) + + # correct elapsed time when seeking + if ( + queue.current_item + and queue.current_item.streamdetails + and queue.current_item.streamdetails.seek_position + and player.state in (PlayerState.PLAYING, PlayerState.PAUSED) + and not queue.flow_mode + ): + queue.elapsed_time += queue.current_item.streamdetails.seek_position + + # enqueue next track if needed + if ( + queue.state == PlayerState.PLAYING + and queue.next_item is not None + and not queue.next_track_enqueued + and queue.corrected_elapsed_time > 2 + ): + self._check_enqueue_next(queue) + + # basic throttle: do not send state changed events if queue did not actually change + prev_state = self._prev_states.get( + queue_id, + CompareState( + queue_id=queue_id, + state=PlayerState.IDLE, + current_index=None, + elapsed_time=0, + stream_title=None, + ), + ) + new_state = CompareState( + queue_id=queue_id, + state=queue.state, + current_index=queue.current_index, + elapsed_time=queue.elapsed_time, + stream_title=queue.current_item.streamdetails.stream_title + if queue.current_item and queue.current_item.streamdetails + else None, + content_type=queue.current_item.streamdetails.audio_format.output_format_str + if queue.current_item and queue.current_item.streamdetails + else None, + ) + changed_keys = get_changed_keys(prev_state, new_state) + # return early if nothing changed + if len(changed_keys) == 0: + return + + # do not send full updates if only time was updated + if changed_keys == {"elapsed_time"}: + self.mass.signal_event( + EventType.QUEUE_TIME_UPDATED, + object_id=queue_id, + data=queue.elapsed_time, + ) + self._prev_states[queue_id] = new_state + return + + # signal update and store state + self.signal_update(queue_id) + self._prev_states[queue_id] = new_state + + # detect change in current index to report that a item has been played + end_of_queue_reached = ( + prev_state["state"] == PlayerState.PLAYING + and new_state["state"] == PlayerState.IDLE + and queue.current_item is not None + and queue.next_item is None + ) + if ( + prev_state["current_index"] is not None + and (prev_state["current_index"] != new_state["current_index"] or end_of_queue_reached) + and (queue_item := self.get_item(queue_id, prev_state["current_index"])) + and (stream_details := queue_item.streamdetails) + ): + seconds_streamed = prev_state["elapsed_time"] + if music_prov := self.mass.get_provider(stream_details.provider): + if seconds_streamed > 10: + self.mass.create_task(music_prov.on_streamed(stream_details, seconds_streamed)) + if queue_item.media_item and seconds_streamed > 10: + # signal 'media item played' event, + # which is useful for plugins that want to do scrobbling + self.mass.signal_event( + EventType.MEDIA_ITEM_PLAYED, + object_id=queue_item.media_item.uri, + data=round(seconds_streamed, 2), + ) + + if end_of_queue_reached: + # end of queue reached, clear items + self.mass.call_later( + 5, self._check_clear_queue, queue, task_id=f"clear_queue_{queue_id}" + ) + + # clear 'next track enqueued' flag if new track is loaded + if prev_state["current_index"] != new_state["current_index"]: + queue.next_track_enqueued = None + + # watch dynamic radio items refill if needed + if "current_index" in changed_keys: + if ( + queue.dont_stop_the_music_enabled + and queue.enqueued_media_items + and queue.current_index is not None + and (queue.items - queue.current_index) <= 1 + ): + # We have received the last item in the queue and Don't stop the music is enabled + # set the played media item(s) as radio items (which will refill the queue) + # note that this will fail if there are no media items for which we have + # a dynamic radio source. + queue.radio_source = queue.enqueued_media_items + if ( + queue.radio_source + and queue.current_index is not None + and (queue.items - queue.current_index) < 5 + ): + task_id = f"fill_radio_tracks_{queue_id}" + self.mass.call_later(5, self._fill_radio_tracks, queue_id, task_id=task_id) + + def on_player_remove(self, player_id: str) -> None: + """Call when a player is removed from the registry.""" + self.mass.create_task(self.mass.cache.delete(f"queue.state.{player_id}")) + self.mass.create_task(self.mass.cache.delete(f"queue.items.{player_id}")) + self._queues.pop(player_id, None) + self._queue_items.pop(player_id, None) + + async def load_next_item( + self, + queue_id: str, + current_item_id_or_index: str | int | None = None, + ) -> QueueItem: + """Call when a player wants to (pre)load the next item into the buffer. + + Raises QueueEmpty if there are no more tracks left. + """ + queue = self.get(queue_id) + if not queue: + msg = f"PlayerQueue {queue_id} is not available" + raise PlayerUnavailableError(msg) + if current_item_id_or_index is None: + cur_index = queue.index_in_buffer or queue.current_index or 0 + elif isinstance(current_item_id_or_index, str): + cur_index = self.index_by_id(queue_id, current_item_id_or_index) + else: + cur_index = current_item_id_or_index + idx = 0 + while True: + next_item: QueueItem | None = None + next_index = self._get_next_index(queue_id, cur_index + idx) + if next_index is None: + raise QueueEmpty("No more tracks left in the queue.") + queue_item = self.get_item(queue_id, next_index) + if queue_item is None: + raise QueueEmpty("No more tracks left in the queue.") + + # work out if we are playing an album and if we should prefer album loudness + if ( + next_index is not None + and (next_item := self.get_item(queue_id, next_index)) + and ( + queue_item.media_item + and hasattr(queue_item.media_item, "album") + and queue_item.media_item.album + and next_item.media_item + and hasattr(next_item.media_item, "album") + and next_item.media_item.album + and queue_item.media_item.album.item_id == next_item.media_item.album.item_id + ) + ): + prefer_album_loudness = True + else: + prefer_album_loudness = False + + try: + # Check if the QueueItem is playable. For example, YT Music returns Radio Items + # that are not playable which will stop playback. + queue_item.streamdetails = await get_stream_details( + mass=self.mass, + queue_item=queue_item, + prefer_album_loudness=prefer_album_loudness, + ) + # Ensure we have at least an image for the queue item, + # so grab full item if needed. Note that for YTM this is always needed + # because it has poor thumbs by default (..sigh) + if queue_item.media_item and ( + not queue_item.media_item.image + or queue_item.media_item.provider.startswith("ytmusic") + ): + queue_item.media_item = await self.mass.music.get_item_by_uri(queue_item.uri) + # allow stripping silence from the begin/end of the track if crossfade is enabled + # this will allow for (much) smoother crossfades + if await self.mass.config.get_player_config_value(queue_id, CONF_CROSSFADE): + queue_item.streamdetails.strip_silence_end = True + queue_item.streamdetails.strip_silence_begin = True + # we're all set, this is our next item + next_item = queue_item + break + except MediaNotFoundError: + # No stream details found, skip this QueueItem + self.logger.debug("Skipping unplayable item: %s", next_item) + queue_item.streamdetails = StreamDetails( + provider=queue_item.media_item.provider if queue_item.media_item else "unknown", + item_id=queue_item.media_item.item_id if queue_item.media_item else "unknown", + audio_format=AudioFormat(), + media_type=queue_item.media_type, + seconds_streamed=0, + ) + idx += 1 + if next_item is None: + raise QueueEmpty("No more (playable) tracks left in the queue.") + return next_item + + def track_loaded_in_buffer(self, queue_id: str, item_id: str) -> None: + """Call when a player has (started) loading a track in the buffer.""" + queue = self.get(queue_id) + if not queue: + msg = f"PlayerQueue {queue_id} is not available" + raise PlayerUnavailableError(msg) + # store the index of the item that is currently (being) loaded in the buffer + # which helps us a bit to determine how far the player has buffered ahead + queue.index_in_buffer = self.index_by_id(queue_id, item_id) + if queue.flow_mode: + return # nothing to do when flow mode is active + self.signal_update(queue_id) + + # Main queue manipulation methods + + def load( + self, + queue_id: str, + queue_items: list[QueueItem], + insert_at_index: int = 0, + keep_remaining: bool = True, + keep_played: bool = True, + shuffle: bool = False, + ) -> None: + """Load new items at index. + + - queue_id: id of the queue to process this request. + - queue_items: a list of QueueItems + - insert_at_index: insert the item(s) at this index + - keep_remaining: keep the remaining items after the insert + - shuffle: (re)shuffle the items after insert index + """ + prev_items = self._queue_items[queue_id][:insert_at_index] if keep_played else [] + next_items = queue_items + + # if keep_remaining, append the old 'next' items + if keep_remaining: + next_items += self._queue_items[queue_id][insert_at_index:] + + # we set the original insert order as attribute so we can un-shuffle + for index, item in enumerate(next_items): + item.sort_index += insert_at_index + index + # (re)shuffle the final batch if needed + if shuffle: + next_items = random.sample(next_items, len(next_items)) + self.update_items(queue_id, prev_items + next_items) + + def update_items(self, queue_id: str, queue_items: list[QueueItem]) -> None: + """Update the existing queue items, mostly caused by reordering.""" + self._queue_items[queue_id] = queue_items + self._queues[queue_id].items = len(self._queue_items[queue_id]) + self.signal_update(queue_id, True) + self._queues[queue_id].next_track_enqueued = None + + # Helper methods + + def get_item(self, queue_id: str, item_id_or_index: int | str | None) -> QueueItem | None: + """Get queue item by index or item_id.""" + if item_id_or_index is None: + return None + queue_items = self._queue_items[queue_id] + if isinstance(item_id_or_index, int) and len(queue_items) > item_id_or_index: + return queue_items[item_id_or_index] + if isinstance(item_id_or_index, str): + return next((x for x in queue_items if x.queue_item_id == item_id_or_index), None) + return None + + def signal_update(self, queue_id: str, items_changed: bool = False) -> None: + """Signal state changed of given queue.""" + queue = self._queues[queue_id] + if items_changed: + self.mass.signal_event(EventType.QUEUE_ITEMS_UPDATED, object_id=queue_id, data=queue) + # save items in cache + self.mass.create_task( + self.mass.cache.set( + "items", + [x.to_cache() for x in self._queue_items[queue_id]], + category=CacheCategory.PLAYER_QUEUE_STATE, + base_key=queue_id, + ) + ) + # always send the base event + self.mass.signal_event(EventType.QUEUE_UPDATED, object_id=queue_id, data=queue) + # save state + self.mass.create_task( + self.mass.cache.set( + "state", + queue.to_cache(), + category=CacheCategory.PLAYER_QUEUE_STATE, + base_key=queue_id, + ) + ) + + def index_by_id(self, queue_id: str, queue_item_id: str) -> int | None: + """Get index by queue_item_id.""" + queue_items = self._queue_items[queue_id] + for index, item in enumerate(queue_items): + if item.queue_item_id == queue_item_id: + return index + return None + + def player_media_from_queue_item(self, queue_item: QueueItem, flow_mode: bool) -> PlayerMedia: + """Parse PlayerMedia from QueueItem.""" + media = PlayerMedia( + uri=self.mass.streams.resolve_stream_url(queue_item, flow_mode=flow_mode), + media_type=MediaType.FLOW_STREAM if flow_mode else queue_item.media_type, + title="Music Assistant" if flow_mode else queue_item.name, + image_url=MASS_LOGO_ONLINE, + duration=queue_item.duration, + queue_id=queue_item.queue_id, + queue_item_id=queue_item.queue_item_id, + ) + if not flow_mode and queue_item.media_item: + media.title = queue_item.media_item.name + media.artist = getattr(queue_item.media_item, "artist_str", "") + media.album = ( + album.name if (album := getattr(queue_item.media_item, "album", None)) else "" + ) + if queue_item.image: + media.image_url = self.mass.metadata.get_image_url(queue_item.image) + return media + + async def get_artist_tracks(self, artist: Artist) -> list[Track]: + """Return tracks for given artist, based on user preference.""" + artist_items_conf = self.mass.config.get_raw_core_config_value( + self.domain, + CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, + ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, + ) + self.logger.debug( + "Fetching tracks to play for artist %s", + artist.name, + ) + if artist_items_conf in ("library_tracks", "all_tracks"): + all_items = await self.mass.music.artists.tracks( + artist.item_id, + artist.provider, + in_library_only=artist_items_conf == "library_tracks", + ) + random.shuffle(all_items) + return all_items + + if artist_items_conf in ("library_album_tracks", "all_album_tracks"): + all_items: list[Track] = [] + for library_album in await self.mass.music.artists.albums( + artist.item_id, + artist.provider, + in_library_only=artist_items_conf == "library_album_tracks", + ): + for album_track in await self.mass.music.albums.tracks( + library_album.item_id, library_album.provider + ): + if album_track not in all_items: + all_items.append(album_track) + random.shuffle(all_items) + return all_items + + return [] + + async def get_album_tracks(self, album: Album, start_item: str | None) -> list[Track]: + """Return tracks for given album, based on user preference.""" + album_items_conf = self.mass.config.get_raw_core_config_value( + self.domain, + CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, + ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, + ) + result: list[Track] = [] + start_item_found = False + self.logger.debug( + "Fetching tracks to play for album %s", + album.name, + ) + for album_track in await self.mass.music.albums.tracks( + item_id=album.item_id, + provider_instance_id_or_domain=album.provider, + in_library_only=album_items_conf == "library_tracks", + ): + if not album_track.available: + continue + if start_item in (album_track.item_id, album_track.uri): + start_item_found = True + if start_item is not None and not start_item_found: + continue + result.append(album_track) + return result + + async def get_playlist_tracks(self, playlist: Playlist, start_item: str | None) -> list[Track]: + """Return tracks for given playlist, based on user preference.""" + result: list[Track] = [] + start_item_found = False + self.logger.debug( + "Fetching tracks to play for playlist %s", + playlist.name, + ) + # TODO: Handle other sort options etc. + async for playlist_track in self.mass.music.playlists.tracks( + playlist.item_id, playlist.provider + ): + if not playlist_track.available: + continue + if start_item in (playlist_track.item_id, playlist_track.uri): + start_item_found = True + if start_item is not None and not start_item_found: + continue + result.append(playlist_track) + return result + + def _get_next_index( + self, queue_id: str, cur_index: int | None, is_skip: bool = False, allow_repeat: bool = True + ) -> int | None: + """ + Return the next index for the queue, accounting for repeat settings. + + Will return None if there are no (more) items in the queue. + """ + queue = self._queues[queue_id] + queue_items = self._queue_items[queue_id] + if not queue_items or cur_index is None: + # queue is empty + return None + # handle repeat single track + if queue.repeat_mode == RepeatMode.ONE and not is_skip: + return cur_index if allow_repeat else None + # handle cur_index is last index of the queue + if cur_index >= (len(queue_items) - 1): + if allow_repeat and queue.repeat_mode == RepeatMode.ALL: + # if repeat all is enabled, we simply start again from the beginning + return 0 + return None + # all other: just the next index + return cur_index + 1 + + def _get_next_item(self, queue_id: str, cur_index: int | None = None) -> QueueItem | None: + """Return next QueueItem for given queue.""" + if (next_index := self._get_next_index(queue_id, cur_index)) is not None: + return self.get_item(queue_id, next_index) + return None + + async def _fill_radio_tracks(self, queue_id: str) -> None: + """Fill a Queue with (additional) Radio tracks.""" + tracks = await self._get_radio_tracks(queue_id=queue_id, is_initial_radio_mode=False) + # fill queue - filter out unavailable items + queue_items = [QueueItem.from_media_item(queue_id, x) for x in tracks if x.available] + self.load( + queue_id, + queue_items, + insert_at_index=len(self._queue_items[queue_id]) + 1, + ) + + def _check_enqueue_next(self, queue: PlayerQueue) -> None: + """Enqueue the next item in the queue (if needed).""" + if queue.flow_mode: + return + if queue.next_item is None: + return + if queue.next_track_enqueued == queue.next_item.queue_item_id: + return + + async def _enqueue_next(): + next_item = await self.load_next_item(queue.queue_id, queue.current_index) + queue.next_track_enqueued = next_item.queue_item_id + await self.mass.players.enqueue_next_media( + player_id=queue.queue_id, + media=self.player_media_from_queue_item(next_item, False), + ) + + self.mass.create_task(_enqueue_next()) + + async def _get_radio_tracks( + self, queue_id: str, is_initial_radio_mode: bool = False + ) -> list[Track]: + """Call the registered music providers for dynamic tracks.""" + queue = self._queues[queue_id] + if not queue.radio_source: + # this may happen during race conditions as this method is called delayed + return None + available_base_tracks: list[Track] = [] + base_track_sample_size = 5 + # Grab all the available base tracks based on the selected source items. + # shuffle the source items, just in case + for radio_item in random.sample(queue.radio_source, len(queue.radio_source)): + ctrl = self.mass.music.get_controller(radio_item.media_type) + try: + available_base_tracks += [ + track + for track in await ctrl.dynamic_base_tracks( + radio_item.item_id, radio_item.provider + ) + # Avoid duplicate base tracks + if track not in available_base_tracks + ] + except UnsupportedFeaturedException: + self.logger.debug( + "Skip loading radio items for %s: - " + "Provider %s does not support dynamic (base) tracks", + radio_item.uri, + radio_item.provider, + ) + # Sample tracks from the base tracks, which will be used to calculate the dynamic ones + base_tracks = random.sample( + available_base_tracks, min(base_track_sample_size, len(available_base_tracks)) + ) + # Use a set to avoid duplicate dynamic tracks + dynamic_tracks: set[Track] = set() + track_ctrl = self.mass.music.get_controller(MediaType.TRACK) + # Use base tracks + Trackcontroller to obtain similar tracks for every base Track + for base_track in base_tracks: + [ + dynamic_tracks.add(track) + for track in await track_ctrl.get_provider_similar_tracks( + base_track.item_id, base_track.provider + ) + if track not in base_tracks + # Ignore tracks that are too long for radio mode, e.g. mixes + and track.duration <= RADIO_TRACK_MAX_DURATION_SECS + ] + if len(dynamic_tracks) >= 50: + break + queue_tracks: list[Track] = [] + dynamic_tracks = list(dynamic_tracks) + # Only include the sampled base tracks when the radio mode is first initialized + if is_initial_radio_mode: + queue_tracks += [base_tracks[0]] + # Exhaust base tracks with the pattern of BDDBDDBDD (1 base track + 2 dynamic tracks) + if len(base_tracks) > 1: + for base_track in base_tracks[1:]: + queue_tracks += [base_track] + queue_tracks += random.sample(dynamic_tracks, 2) + # Add dynamic tracks to the queue, make sure to exclude already picked tracks + remaining_dynamic_tracks = [t for t in dynamic_tracks if t not in queue_tracks] + queue_tracks += random.sample( + remaining_dynamic_tracks, min(len(remaining_dynamic_tracks), 25) + ) + return queue_tracks + + async def _check_clear_queue(self, queue: PlayerQueue) -> None: + """Check if the queue should be cleared after the current item.""" + for _ in range(5): + await asyncio.sleep(1) + if queue.state != PlayerState.IDLE: + return + if queue.next_item is not None: + return + if not (queue.current_index >= len(self._queue_items[queue.queue_id]) - 1): + return + self.logger.info("End of queue reached, clearing items") + self.clear(queue.queue_id) + + def _get_flow_queue_stream_index( + self, queue: PlayerQueue, player: Player + ) -> tuple[int | None, int]: + """Calculate current queue index and current track elapsed time when flow mode is active.""" + elapsed_time_queue_total = player.corrected_elapsed_time or 0 + if queue.current_index is None: + return None, elapsed_time_queue_total + + # For each track that has been streamed/buffered to the player, + # a playlog entry will be created with the queue item id + # and the amount of seconds streamed. We traverse the playlog to figure + # out where we are in the queue, accounting for actual streamed + # seconds (and not duration) and skipped seconds. If a track has been repeated, + # it will simply be in the playlog multiple times. + played_time = 0 + queue_index = queue.current_index or 0 + track_time = 0 + for play_log_entry in queue.flow_mode_stream_log: + queue_item_duration = ( + # NOTE: 'seconds_streamed' can actually be 0 if there was a stream error! + play_log_entry.seconds_streamed + if play_log_entry.seconds_streamed is not None + else play_log_entry.duration + ) + if elapsed_time_queue_total > (queue_item_duration + played_time): + # total elapsed time is more than (streamed) track duration + # this track has been fully played, move in. + played_time += queue_item_duration + else: + # no more seconds left to divide, this is our track + # account for any seeking by adding the skipped/seeked seconds + queue_index = self.index_by_id(queue.queue_id, play_log_entry.queue_item_id) + queue_item = self.get_item(queue.queue_id, queue_index) + if queue_item and queue_item.streamdetails: + track_sec_skipped = queue_item.streamdetails.seek_position + else: + track_sec_skipped = 0 + track_time = elapsed_time_queue_total + track_sec_skipped - played_time + break + + return queue_index, track_time + + def _parse_player_current_item_id(self, queue_id: str, player: Player) -> str | None: + """Parse QueueItem ID from Player's current url.""" + if not player.current_media: + return None + if player.current_media.queue_id and player.current_media.queue_id != queue_id: + return None + if player.current_media.queue_item_id: + return player.current_media.queue_item_id + if not player.current_media.uri: + return None + if queue_id in player.current_media.uri: + # try to extract the item id from either a url or queue_id/item_id combi + current_item_id = player.current_media.uri.rsplit("/")[-1].split(".")[0] + if self.get_item(queue_id, current_item_id): + return current_item_id + return None diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py new file mode 100644 index 00000000..24c8d282 --- /dev/null +++ b/music_assistant/controllers/players.py @@ -0,0 +1,1311 @@ +""" +MusicAssistant Players Controller. + +Handles all logic to control supported players, +which are provided by Player Providers. + +""" + +from __future__ import annotations + +import asyncio +import functools +import time +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast + +from music_assistant_models.enums import ( + EventType, + MediaType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderType, +) +from music_assistant_models.errors import ( + AlreadyRegisteredError, + PlayerCommandFailed, + PlayerUnavailableError, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import UniqueList +from music_assistant_models.player import Player, PlayerMedia + +from music_assistant.constants import ( + CONF_AUTO_PLAY, + CONF_ENTRY_ANNOUNCE_VOLUME, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN, + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, + CONF_ENTRY_PLAYER_ICON, + CONF_ENTRY_PLAYER_ICON_GROUP, + CONF_HIDE_PLAYER, + CONF_PLAYERS, + CONF_TTS_PRE_ANNOUNCE, +) +from music_assistant.helpers.api import api_command +from music_assistant.helpers.tags import parse_tags +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.helpers.util import TaskManager, get_changed_values +from music_assistant.models.core_controller import CoreController +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.player_group import PlayerGroupProvider + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable, Coroutine, Iterator + + from music_assistant_models.config_entries import CoreConfig, PlayerConfig + + +_PlayerControllerT = TypeVar("_PlayerControllerT", bound="PlayerController") +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def handle_player_command( + func: Callable[Concatenate[_PlayerControllerT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_PlayerControllerT, _P], Coroutine[Any, Any, _R | None]]: + """Check and log commands to players.""" + + @functools.wraps(func) + async def wrapper(self: _PlayerControllerT, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: + """Log and handle_player_command commands to players.""" + player_id = kwargs["player_id"] if "player_id" in kwargs else args[0] + if (player := self._players.get(player_id)) is None or not player.available: + # player not existent + self.logger.warning( + "Ignoring command %s for unavailable player %s", + func.__name__, + player_id, + ) + return + + self.logger.debug( + "Handling command %s for player %s", + func.__name__, + player.display_name, + ) + try: + await func(self, *args, **kwargs) + except Exception as err: + raise PlayerCommandFailed(str(err)) from err + + return wrapper + + +class PlayerController(CoreController): + """Controller holding all logic to control registered players.""" + + domain: str = "players" + + def __init__(self, *args, **kwargs) -> None: + """Initialize core controller.""" + super().__init__(*args, **kwargs) + self._players: dict[str, Player] = {} + self._prev_states: dict[str, dict] = {} + self.manifest.name = "Players controller" + self.manifest.description = ( + "Music Assistant's core controller which manages all players from all providers." + ) + self.manifest.icon = "speaker-multiple" + self._poll_task: asyncio.Task | None = None + self._player_throttlers: dict[str, Throttler] = {} + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + self._poll_task = self.mass.create_task(self._poll_players()) + + async def close(self) -> None: + """Cleanup on exit.""" + if self._poll_task and not self._poll_task.done(): + self._poll_task.cancel() + + @property + def providers(self) -> list[PlayerProvider]: + """Return all loaded/running MusicProviders.""" + return self.mass.get_providers(ProviderType.MUSIC) # type: ignore=return-value + + def __iter__(self) -> Iterator[Player]: + """Iterate over (available) players.""" + return iter(self._players.values()) + + @api_command("players/all") + def all( + self, + return_unavailable: bool = True, + return_disabled: bool = False, + ) -> tuple[Player, ...]: + """Return all registered players.""" + return tuple( + player + for player in self._players.values() + if (player.available or return_unavailable) and (player.enabled or return_disabled) + ) + + @api_command("players/get") + def get( + self, + player_id: str, + raise_unavailable: bool = False, + ) -> Player | None: + """Return Player by player_id.""" + if player := self._players.get(player_id): + if (not player.available or not player.enabled) and raise_unavailable: + msg = f"Player {player_id} is not available" + raise PlayerUnavailableError(msg) + return player + if raise_unavailable: + msg = f"Player {player_id} is not available" + raise PlayerUnavailableError(msg) + return None + + @api_command("players/get_by_name") + def get_by_name(self, name: str) -> Player | None: + """Return Player by name or None if no match is found.""" + return next((x for x in self._players.values() if x.name == name), None) + + # Player commands + + @api_command("players/cmd/stop") + @handle_player_command + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player. + + - 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.stop(active_queue.queue_id) + return + # send to player provider + async with self._player_throttlers[player.player_id]: + if player_provider := self.get_player_provider(player.player_id): + await player_provider.cmd_stop(player.player_id) + + @api_command("players/cmd/play") + @handle_player_command + async def cmd_play(self, player_id: str) -> None: + """Send PLAY (unpause) command to given player. + + - 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 + active_source = player.active_source or player.player_id + if (active_queue := self.mass.player_queues.get(active_source)) and active_queue.items: + await self.mass.player_queues.play(active_queue.queue_id) + return + # send to player provider + player_provider = self.get_player_provider(player.player_id) + async with self._player_throttlers[player.player_id]: + await player_provider.cmd_play(player.player_id) + + @api_command("players/cmd/pause") + @handle_player_command + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player. + + - player_id: player_id of the player to handle the command. + """ + player = self._get_player_with_redirect(player_id) + if player.announcement_in_progress: + self.logger.warning("Ignore command: An announcement is in progress") + return + if PlayerFeature.PAUSE not in player.supported_features: + # if player does not support pause, we need to send stop + self.logger.info( + "Player %s does not support pause, using STOP instead", player.display_name + ) + await self.cmd_stop(player.player_id) + return + 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. + + - player_id: player_id of the player to handle the command. + """ + player = self._get_player_with_redirect(player_id) + if player.state == PlayerState.PLAYING: + await self.cmd_pause(player.player_id) + else: + await self.cmd_play(player.player_id) + + @api_command("players/cmd/seek") + async def cmd_seek(self, player_id: str, position: int) -> None: + """Handle SEEK command for given player. + + - player_id: player_id of the player to handle the command. + - position: position in seconds to seek to in the current playing item. + """ + player = self._get_player_with_redirect(player_id) + # Redirect to queue controller if it is active + active_source = player.active_source or player.player_id + if active_queue := self.mass.player_queues.get(active_source): + await self.mass.player_queues.seek(active_queue.queue_id, position) + return + if PlayerFeature.SEEK not in player.supported_features: + msg = f"Player {player.display_name} does not support seeking" + raise UnsupportedFeaturedException(msg) + player_prov = self.get_player_provider(player.player_id) + await player_prov.cmd_seek(player.player_id, position) + + @api_command("players/cmd/next") + async def cmd_next_track(self, player_id: str) -> None: + """Handle NEXT TRACK command for given player.""" + player = self._get_player_with_redirect(player_id) + # Redirect to queue controller if it is active + active_source = player.active_source or player.player_id + if active_queue := self.mass.player_queues.get(active_source): + await self.mass.player_queues.next(active_queue.queue_id) + return + if PlayerFeature.NEXT_PREVIOUS not in player.supported_features: + msg = f"Player {player.display_name} does not support skipping to the next track." + raise UnsupportedFeaturedException(msg) + player_prov = self.get_player_provider(player.player_id) + await player_prov.cmd_next(player.player_id) + + @api_command("players/cmd/previous") + async def cmd_previous_track(self, player_id: str) -> None: + """Handle PREVIOUS TRACK command for given player.""" + player = self._get_player_with_redirect(player_id) + # Redirect to queue controller if it is active + active_source = player.active_source or player.player_id + if active_queue := self.mass.player_queues.get(active_source): + await self.mass.player_queues.previous(active_queue.queue_id) + return + if PlayerFeature.NEXT_PREVIOUS not in player.supported_features: + msg = f"Player {player.display_name} does not support skipping to the previous track." + raise UnsupportedFeaturedException(msg) + player_prov = self.get_player_provider(player.player_id) + await player_prov.cmd_previous(player.player_id) + + @api_command("players/cmd/power") + @handle_player_command + async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = False) -> None: + """Send POWER command to given player. + + - player_id: player_id of the player to handle the command. + - powered: bool if player should be powered on or off. + """ + player = self.get(player_id, True) + + if player.powered == powered: + return # nothing to do + + # unsync player at power off + player_was_synced = player.synced_to is not None + if not powered and (player.synced_to): + await self.cmd_unsync(player_id) + + # always stop player at power off + if ( + not powered + and not player_was_synced + and player.state in (PlayerState.PLAYING, PlayerState.PAUSED) + ): + await self.cmd_stop(player_id) + + # power off all synced childs when player is a sync leader + elif not powered and player.type == PlayerType.PLAYER and player.group_childs: + async with TaskManager(self.mass) as tg: + for member in self.iter_group_members(player, True): + tg.create_task(self.cmd_power(member.player_id, False)) + + # handle actual power command + if PlayerFeature.POWER in player.supported_features: + # player supports power command: forward to player provider + player_provider = self.get_player_provider(player_id) + async with self._player_throttlers[player_id]: + await player_provider.cmd_power(player_id, powered) + else: + # allow the stop command to process and prevent race conditions + await asyncio.sleep(0.2) + await self.mass.cache.set(player_id, powered, base_key="player_power") + + # always optimistically set the power state to update the UI + # as fast as possible and prevent race conditions + player.powered = powered + # reset active source on power off + if not powered: + player.active_source = None + + if not skip_update: + self.update(player_id) + + # handle 'auto play on power on' feature + if ( + not player.active_group + and powered + and self.mass.config.get_raw_player_config_value(player_id, CONF_AUTO_PLAY, False) + and player.active_source in (None, player_id) + ): + await self.mass.player_queues.resume(player_id) + + @api_command("players/cmd/volume_set") + @handle_player_command + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player. + + - player_id: player_id of the player to handle the command. + - volume_level: volume level (0..100) to set on the player. + """ + # TODO: Implement PlayerControl + player = self.get(player_id, True) + if player.type == PlayerType.GROUP: + # redirect to group volume control + await self.cmd_group_volume(player_id, volume_level) + return + if PlayerFeature.VOLUME_SET not in player.supported_features: + msg = f"Player {player.display_name} does not support volume_set" + raise UnsupportedFeaturedException(msg) + player_provider = self.get_player_provider(player_id) + async with self._player_throttlers[player_id]: + await player_provider.cmd_volume_set(player_id, volume_level) + + @api_command("players/cmd/volume_up") + @handle_player_command + async def cmd_volume_up(self, player_id: str) -> None: + """Send VOLUME_UP command to given player. + + - player_id: player_id of the player to handle the command. + """ + if not (player := self.get(player_id)): + return + if player.volume_level < 5 or player.volume_level > 95: + step_size = 1 + elif player.volume_level < 20 or player.volume_level > 80: + step_size = 2 + else: + step_size = 5 + new_volume = min(100, self._players[player_id].volume_level + step_size) + await self.cmd_volume_set(player_id, new_volume) + + @api_command("players/cmd/volume_down") + @handle_player_command + async def cmd_volume_down(self, player_id: str) -> None: + """Send VOLUME_DOWN command to given player. + + - player_id: player_id of the player to handle the command. + """ + if not (player := self.get(player_id)): + return + if player.volume_level < 5 or player.volume_level > 95: + step_size = 1 + elif player.volume_level < 20 or player.volume_level > 80: + step_size = 2 + else: + step_size = 5 + new_volume = max(0, self._players[player_id].volume_level - step_size) + await self.cmd_volume_set(player_id, new_volume) + + @api_command("players/cmd/group_volume") + @handle_player_command + async def cmd_group_volume(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given playergroup. + + Will send the new (average) volume level to group child's. + - player_id: player_id of the playergroup to handle the command. + - volume_level: volume level (0..100) to set on the player. + """ + group_player = self.get(player_id, True) + assert group_player + # handle group volume by only applying the volume to powered members + cur_volume = group_player.group_volume + new_volume = volume_level + volume_dif = new_volume - cur_volume + coros = [] + for child_player in self.iter_group_members( + group_player, only_powered=True, exclude_self=False + ): + if PlayerFeature.VOLUME_SET not in child_player.supported_features: + continue + cur_child_volume = child_player.volume_level + new_child_volume = int(cur_child_volume + volume_dif) + new_child_volume = max(0, new_child_volume) + new_child_volume = min(100, new_child_volume) + coros.append(self.cmd_volume_set(child_player.player_id, new_child_volume)) + await asyncio.gather(*coros) + + @api_command("players/cmd/group_volume_up") + @handle_player_command + async def cmd_group_volume_up(self, player_id: str) -> None: + """Send VOLUME_UP command to given playergroup. + + - player_id: player_id of the player to handle the command. + """ + group_player = self.get(player_id, True) + assert group_player + cur_volume = group_player.group_volume + if cur_volume < 5 or cur_volume > 95: + step_size = 1 + elif cur_volume < 20 or cur_volume > 80: + step_size = 2 + else: + step_size = 5 + new_volume = min(100, cur_volume + step_size) + await self.cmd_group_volume(player_id, new_volume) + + @api_command("players/cmd/group_volume_down") + @handle_player_command + async def cmd_group_volume_down(self, player_id: str) -> None: + """Send VOLUME_DOWN command to given playergroup. + + - player_id: player_id of the player to handle the command. + """ + group_player = self.get(player_id, True) + assert group_player + cur_volume = group_player.group_volume + if cur_volume < 5 or cur_volume > 95: + step_size = 1 + elif cur_volume < 20 or cur_volume > 80: + step_size = 2 + else: + step_size = 5 + new_volume = max(0, cur_volume - step_size) + await self.cmd_group_volume(player_id, new_volume) + + @api_command("players/cmd/volume_mute") + @handle_player_command + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME_MUTE command to given player. + + - player_id: player_id of the player to handle the command. + - muted: bool if player should be muted. + """ + player = self.get(player_id, True) + assert player + if PlayerFeature.VOLUME_MUTE not in player.supported_features: + self.logger.info( + "Player %s does not support muting, using volume instead", player.display_name + ) + if muted: + player._prev_volume_level = player.volume_level + player.volume_muted = True + await self.cmd_volume_set(player_id, 0) + else: + player.volume_muted = False + await self.cmd_volume_set(player_id, player._prev_volume_level) + return + player_provider = self.get_player_provider(player_id) + async with self._player_throttlers[player_id]: + await player_provider.cmd_volume_mute(player_id, muted) + + @api_command("players/cmd/play_announcement") + async def play_announcement( + self, + player_id: str, + url: str, + use_pre_announce: bool | None = None, + volume_level: int | None = None, + ) -> None: + """Handle playback of an announcement (url) on given player.""" + player = self.get(player_id, True) + if not url.startswith("http"): + raise PlayerCommandFailed("Only URLs are supported for announcements") + if player.announcement_in_progress: + raise PlayerCommandFailed( + f"An announcement is already in progress to player {player.display_name}" + ) + try: + # mark announcement_in_progress on player + player.announcement_in_progress = True + # determine if the player has native announcements support + native_announce_support = PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features + # determine pre-announce from (group)player config + if use_pre_announce is None and "tts" in url: + use_pre_announce = await self.mass.config.get_player_config_value( + player_id, + CONF_TTS_PRE_ANNOUNCE, + ) + # if player type is group with all members supporting announcements, + # we forward the request to each individual player + if player.type == PlayerType.GROUP and ( + all( + PlayerFeature.PLAY_ANNOUNCEMENT in x.supported_features + for x in self.iter_group_members(player) + ) + ): + # forward the request to each individual player + async with TaskManager(self.mass) as tg: + for group_member in player.group_childs: + tg.create_task( + self.play_announcement( + group_member, + url=url, + use_pre_announce=use_pre_announce, + volume_level=volume_level, + ) + ) + return + self.logger.info( + "Playback announcement to player %s (with pre-announce: %s): %s", + player.display_name, + use_pre_announce, + url, + ) + # create a PlayerMedia object for the announcement so + # we can send a regular play-media call downstream + announcement = PlayerMedia( + uri=self.mass.streams.get_announcement_url(player_id, url, use_pre_announce), + media_type=MediaType.ANNOUNCEMENT, + title="Announcement", + custom_data={"url": url, "use_pre_announce": use_pre_announce}, + ) + # handle native announce support + if native_announce_support: + if prov := self.mass.get_provider(player.provider): + await prov.play_announcement(player_id, announcement, volume_level) + return + # use fallback/default implementation + await self._play_announcement(player, announcement, volume_level) + finally: + player.announcement_in_progress = False + + @handle_player_command + async def play_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle PLAY MEDIA on given player. + + - player_id: player_id of the player to handle the command. + - media: The Media that needs to be played on the player. + """ + player = self._get_player_with_redirect(player_id) + # power on the player if needed + if not player.powered: + await self.cmd_power(player.player_id, True) + player_prov = self.get_player_provider(player.player_id) + await player_prov.play_media( + player_id=player.player_id, + media=media, + ) + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of a next media item on the player.""" + player = self.get(player_id, raise_unavailable=True) + if PlayerFeature.ENQUEUE not in player.supported_features: + raise UnsupportedFeaturedException( + f"Player {player.display_name} does not support enqueueing" + ) + player_prov = self.mass.get_provider(player.provider) + async with self._player_throttlers[player_id]: + await player_prov.enqueue_next_media(player_id=player_id, media=media) + + @api_command("players/cmd/sync") + @handle_player_command + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (leader) player/sync group. + If the player is already synced to another player, it will be unsynced there first. + If the target player itself is already synced to another player, this may fail. + If the player can not be synced with the given target player, this may fail. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup leader or group player. + """ + await self.cmd_sync_many(target_player, [player_id]) + + @api_command("players/cmd/unsync") + @handle_player_command + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + If the player is not currently synced to any other player, + this will silently be ignored. + + - player_id: player_id of the player to handle the command. + """ + if not (player := self.get(player_id)): + self.logger.warning("Player %s is not available", player_id) + return + if PlayerFeature.SYNC not in player.supported_features: + self.logger.warning("Player %s does not support (un)sync commands", player.name) + return + if not (player.synced_to or player.group_childs): + return # nothing to do + + if player.active_group and ( + (group_provider := self.get_player_provider(player.active_group)) + and group_provider.domain == "player_group" + ): + # the player is part of a permanent (sync)group and the user tries to unsync + # redirect the command to the group provider + group_provider = cast(PlayerGroupProvider, group_provider) + await group_provider.cmd_unsync_member(player_id, player.active_group) + return + + # handle (edge)case where un unsync command is sent to a sync leader; + # we dissolve the entire syncgroup in this case. + # while maybe not strictly needed to do this for all player providers, + # we do this to keep the functionality consistent across all providers + if player.group_childs: + self.logger.warning( + "Detected unsync command to player %s which is a sync(group) leader, " + "all sync members will be unsynced!", + player.name, + ) + async with TaskManager(self.mass) as tg: + for group_child_id in player.group_childs: + if group_child_id == player_id: + continue + tg.create_task(self.cmd_unsync(group_child_id)) + return + + # (optimistically) reset active source player if it is unsynced + player.active_source = None + + # forward command to the player provider + if player_provider := self.get_player_provider(player_id): + await player_provider.cmd_unsync(player_id) + # if the command succeeded we optimistically reset the sync state + # this is to prevent race conditions and to update the UI as fast as possible + player.synced_to = None + + @api_command("players/cmd/sync_many") + async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: + """Create temporary sync group by joining given players to target player.""" + parent_player: Player = self.get(target_player, True) + prev_group_childs = parent_player.group_childs.copy() + if PlayerFeature.SYNC not in parent_player.supported_features: + msg = f"Player {parent_player.name} does not support sync commands" + raise UnsupportedFeaturedException(msg) + + if parent_player.synced_to: + # guard edge case: player already synced to another player + raise PlayerCommandFailed( + f"Player {parent_player.name} is already synced to another player on its own, " + "you need to unsync it first before you can join other players to it.", + ) + + # filter all player ids on compatibility and availability + final_player_ids: UniqueList[str] = UniqueList() + for child_player_id in child_player_ids: + if child_player_id == target_player: + continue + if not (child_player := self.get(child_player_id)) or not child_player.available: + self.logger.warning("Player %s is not available", child_player_id) + continue + if PlayerFeature.SYNC not in child_player.supported_features: + # this should not happen, but just in case bad things happen, guard it + self.logger.warning("Player %s does not support sync commands", child_player.name) + continue + if child_player.synced_to and child_player.synced_to == target_player: + continue # already synced to this target + + if child_player.group_childs and child_player.state != PlayerState.IDLE: + # guard edge case: childplayer is already a sync leader on its own + raise PlayerCommandFailed( + f"Player {child_player.name} is already synced with other players, " + "you need to unsync it first before you can join it to another player.", + ) + if child_player.synced_to: + # player already synced to another player, unsync first + self.logger.warning( + "Player %s is already synced to another player, unsyncing first", + child_player.name, + ) + await self.cmd_unsync(child_player.player_id) + # power on the player if needed + if not child_player.powered: + await self.cmd_power(child_player.player_id, True, skip_update=True) + # if we reach here, all checks passed + final_player_ids.append(child_player_id) + # set active source if player is synced + child_player.active_source = parent_player.player_id + + # forward command to the player provider after all (base) sanity checks + player_provider = self.get_player_provider(target_player) + async with self._player_throttlers[target_player]: + try: + await player_provider.cmd_sync_many(target_player, final_player_ids) + except Exception: + # restore sync state if the command failed + parent_player.group_childs = prev_group_childs + raise + + @api_command("players/cmd/unsync_many") + async def cmd_unsync_many(self, player_ids: list[str]) -> None: + """Handle UNSYNC command for all the given players.""" + for player_id in list(player_ids): + await self.cmd_unsync(player_id) + + def set(self, player: Player) -> None: + """Set/Update player details on the controller.""" + if player.player_id not in self._players: + # new player + self.register(player) + return + self._players[player.player_id] = player + self.update(player.player_id) + + async def register(self, player: Player) -> None: + """Register a new player on the controller.""" + if self.mass.closing: + return + player_id = player.player_id + + if player_id in self._players: + msg = f"Player {player_id} is already registered" + raise AlreadyRegisteredError(msg) + + # make sure that the player's provider is set to the instance id + if prov := self.mass.get_provider(player.provider): + player.provider = prov.instance_id + else: + raise RuntimeError("Invalid provider ID given: %s", player.provider) + + # make sure a default config exists + self.mass.config.create_default_player_config( + player_id, player.provider, player.name, player.enabled_by_default + ) + + player.enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True) + + # register playerqueue for this player + self.mass.create_task(self.mass.player_queues.on_player_register(player)) + + # register throttler for this player + self._player_throttlers[player_id] = Throttler(1, 0.2) + + self._players[player_id] = player + + # ignore disabled players + if not player.enabled: + return + + # restore powered state from cache + if player.state == PlayerState.PLAYING: + player.powered = True + elif (cache := await self.mass.cache.get(player_id, base_key="player_power")) is not None: + player.powered = cache + + self.logger.info( + "Player registered: %s/%s", + player_id, + player.name, + ) + self.mass.signal_event(EventType.PLAYER_ADDED, object_id=player.player_id, data=player) + # always call update to fix special attributes like display name, group volume etc. + self.update(player.player_id) + + async def register_or_update(self, player: Player) -> None: + """Register a new player on the controller or update existing one.""" + if self.mass.closing: + return + + if player.player_id in self._players: + self._players[player.player_id] = player + self.update(player.player_id) + return + + await self.register(player) + + def remove(self, player_id: str, cleanup_config: bool = True) -> None: + """Remove a player from the player manager.""" + player = self._players.pop(player_id, None) + if player is None: + return + self.logger.info("Player removed: %s", player.name) + self.mass.player_queues.on_player_remove(player_id) + if cleanup_config: + self.mass.config.remove(f"players/{player_id}") + self._prev_states.pop(player_id, None) + self.mass.signal_event(EventType.PLAYER_REMOVED, player_id) + + def update( + self, player_id: str, skip_forward: bool = False, force_update: bool = False + ) -> None: + """Update player state.""" + if self.mass.closing: + return + if player_id not in self._players: + return + player = self._players[player_id] + prev_state = self._prev_states.get(player_id, {}) + player.active_source = self._get_active_source(player) + player.volume_level = player.volume_level or 0 # guard for None volume + # correct group_members if needed + if player.group_childs == {player.player_id}: + player.group_childs = set() + # Auto correct player state if player is synced (or group child) + # This is because some players/providers do not accurately update this info + # for the sync child's. + if player.synced_to and (sync_leader := self.get(player.synced_to)): + player.state = sync_leader.state + player.elapsed_time = sync_leader.elapsed_time + player.elapsed_time_last_updated = sync_leader.elapsed_time_last_updated + # calculate group volume + player.group_volume = self._get_group_volume_level(player) + if player.type == PlayerType.GROUP: + player.volume_level = player.group_volume + # prefer any overridden name from config + player.display_name = ( + self.mass.config.get_raw_player_config_value(player.player_id, "name") + or player.name + or player.player_id + ) + player.hidden = self.mass.config.get_raw_player_config_value( + player.player_id, CONF_HIDE_PLAYER, False + ) + player.icon = self.mass.config.get_raw_player_config_value( + player.player_id, + CONF_ENTRY_PLAYER_ICON.key, + CONF_ENTRY_PLAYER_ICON_GROUP.default_value + if player.type == PlayerType.GROUP + else CONF_ENTRY_PLAYER_ICON.default_value, + ) + + # correct available state if needed + if not player.enabled: + player.available = False + + # basic throttle: do not send state changed events if player did not actually change + new_state = self._players[player_id].to_dict() + changed_values = get_changed_values( + prev_state, + new_state, + ignore_keys=[ + "elapsed_time_last_updated", + "seq_no", + "last_poll", + ], + ) + self._prev_states[player_id] = new_state + + if not player.enabled and not force_update: + # ignore updates for disabled players + return + + # always signal update to the playerqueue (regardless of changes) + self.mass.player_queues.on_player_update(player, changed_values) + + if len(changed_values) == 0 and not force_update: + return + + if changed_values.keys() != {"elapsed_time"} or force_update: + # ignore elapsed_time only changes + self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player) + + if skip_forward and not force_update: + return + + # handle player becoming unavailable + if "available" in changed_values and not player.available: + self._handle_player_unavailable(player) + + # update/signal group player(s) child's when group updates + for child_player in self.iter_group_members(player, exclude_self=True): + self.update(child_player.player_id, skip_forward=True) + # update/signal group player(s) when child updates + for group_player in self._get_player_groups(player, powered_only=False): + if player_prov := self.mass.get_provider(group_player.provider): + self.mass.create_task(player_prov.poll_player(group_player.player_id)) + + def get_player_provider(self, player_id: str) -> PlayerProvider: + """Return PlayerProvider for given player.""" + player = self._players[player_id] + player_provider = self.mass.get_provider(player.provider) + return cast(PlayerProvider, player_provider) + + def get_announcement_volume(self, player_id: str, volume_override: int | None) -> int | None: + """Get the (player specific) volume for a announcement.""" + volume_strategy = self.mass.config.get_raw_player_config_value( + player_id, + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key, + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value, + ) + volume_strategy_volume = self.mass.config.get_raw_player_config_value( + player_id, + CONF_ENTRY_ANNOUNCE_VOLUME.key, + CONF_ENTRY_ANNOUNCE_VOLUME.default_value, + ) + volume_level = volume_override + if volume_level is None and volume_strategy == "absolute": + volume_level = volume_strategy_volume + elif volume_level is None and volume_strategy == "relative": + player = self.get(player_id) + volume_level = player.volume_level + volume_strategy_volume + elif volume_level is None and volume_strategy == "percentual": + player = self.get(player_id) + percentual = (player.volume_level / 100) * volume_strategy_volume + volume_level = player.volume_level + percentual + if volume_level is not None: + announce_volume_min = self.mass.config.get_raw_player_config_value( + player_id, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value, + ) + volume_level = max(announce_volume_min, volume_level) + announce_volume_max = self.mass.config.get_raw_player_config_value( + player_id, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value, + ) + volume_level = min(announce_volume_max, volume_level) + # ensure the result is an integer + return None if volume_level is None else int(volume_level) + + def iter_group_members( + self, + group_player: Player, + only_powered: bool = False, + only_playing: bool = False, + active_only: bool = False, + exclude_self: bool = True, + ) -> Iterator[Player]: + """Get (child) players attached to a group player or syncgroup.""" + for child_id in list(group_player.group_childs): + if child_player := self.get(child_id, False): + if not child_player.available or not child_player.enabled: + continue + if not (not only_powered or child_player.powered): + continue + if not (not active_only or child_player.active_group == group_player.player_id): + continue + if exclude_self and child_player.player_id == group_player.player_id: + continue + if not ( + not only_playing + or child_player.state in (PlayerState.PLAYING, PlayerState.PAUSED) + ): + continue + yield child_player + + async def wait_for_state( + self, + player: Player, + wanted_state: PlayerState, + timeout: float = 60.0, + minimal_time: float = 0, + ) -> None: + """Wait for the given player to reach the given state.""" + start_timestamp = time.time() + self.logger.debug( + "Waiting for player %s to reach state %s", player.display_name, wanted_state + ) + try: + async with asyncio.timeout(timeout): + while player.state != wanted_state: + await asyncio.sleep(0.1) + + except TimeoutError: + self.logger.debug( + "Player %s did not reach state %s within the timeout of %s seconds", + player.display_name, + wanted_state, + timeout, + ) + elapsed_time = round(time.time() - start_timestamp, 2) + if elapsed_time < minimal_time: + self.logger.debug( + "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...", + player.display_name, + wanted_state, + elapsed_time, + minimal_time, + ) + await asyncio.sleep(minimal_time - elapsed_time) + else: + self.logger.debug( + "Player %s reached state %s within %s seconds", + player.display_name, + wanted_state, + elapsed_time, + ) + + async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: + """Call (by config manager) when the configuration of a player changes.""" + player_disabled = "enabled" in changed_keys and not config.enabled + # signal player provider that the config changed + if player_provider := self.mass.get_provider(config.provider): + with suppress(PlayerUnavailableError): + await player_provider.on_player_config_change(config, changed_keys) + if not (player := self.get(config.player_id)): + return + if player_disabled: + # edge case: ensure that the player is powered off if the player gets disabled + await self.cmd_power(config.player_id, False) + player.available = False + # if the player was playing, restart playback + elif not player_disabled and player.state == PlayerState.PLAYING: + self.mass.call_later(1, self.mass.player_queues.resume, player.active_source) + # 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 + group_player = self.get(player.active_group) + with suppress(UnsupportedFeaturedException, PlayerCommandFailed): + await player_provider.set_members( + player.active_group, + [x for x in group_player.group_childs if x != player.player_id], + ) + player.enabled = config.enabled + + def _get_player_with_redirect(self, player_id: str) -> Player: + """Get player with check if playback related command should be redirected.""" + player = self.get(player_id, True) + if player.synced_to and (sync_leader := self.get(player.synced_to)): + self.logger.info( + "Player %s is synced to %s and can not accept " + "playback related commands itself, " + "redirected the command to the sync leader.", + player.name, + sync_leader.name, + ) + return sync_leader + if player.active_group and (active_group := self.get(player.active_group)): + self.logger.info( + "Player %s is part of a playergroup and can not accept " + "playback related commands itself, " + "redirected the command to the group leader.", + player.name, + ) + return active_group + return player + + def _get_player_groups( + self, player: Player, available_only: bool = True, powered_only: bool = False + ) -> Iterator[Player]: + """Return all groupplayers the given player belongs to.""" + for _player in self: + if _player.player_id == player.player_id: + continue + if _player.type != PlayerType.GROUP: + continue + if available_only and not _player.available: + continue + if powered_only and not _player.powered: + continue + if player.player_id in _player.group_childs: + yield _player + + def _get_active_source(self, player: Player) -> str: + """Return the active_source id for given player.""" + # if player is synced, return group leader's active source + if player.synced_to and (parent_player := self.get(player.synced_to)): + return parent_player.active_source + # if player has group active, return those details + if player.active_group and (group_player := self.get(player.active_group)): + return self._get_active_source(group_player) + # defaults to the player's own player id if no active source set + return player.active_source or player.player_id + + def _get_group_volume_level(self, player: Player) -> int: + """Calculate a group volume from the grouped members.""" + if len(player.group_childs) == 0: + # player is not a group or syncgroup + return player.volume_level + # calculate group volume from all (turned on) players + group_volume = 0 + active_players = 0 + for child_player in self.iter_group_members(player, only_powered=True, exclude_self=False): + if PlayerFeature.VOLUME_SET not in child_player.supported_features: + continue + group_volume += child_player.volume_level or 0 + active_players += 1 + if active_players: + group_volume = group_volume / active_players + return int(group_volume) + + def _handle_player_unavailable(self, player: Player) -> None: + """Handle a player becoming unavailable.""" + if player.synced_to: + self.mass.create_task(self.cmd_unsync(player.player_id)) + # also set this optimistically because the above command will most likely fail + player.synced_to = None + return + for group_child_id in player.group_childs: + if group_child_id == player.player_id: + continue + if child_player := self.get(group_child_id): + self.mass.create_task(self.cmd_power(group_child_id, False, True)) + # also set this optimistically because the above command will most likely fail + child_player.synced_to = None + player.group_childs = set() + if player.active_group and (group_player := self.get(player.active_group)): + # remove player from group if its part of a group + group_player = self.get(player.active_group) + if player.player_id in group_player.group_childs: + group_player.group_childs.remove(player.player_id) + + async def _play_announcement( + self, + player: Player, + announcement: PlayerMedia, + volume_level: int | None = None, + ) -> None: + """Handle (default/fallback) implementation of the play announcement feature. + + This default implementation will; + - stop playback of the current media (if needed) + - power on the player (if needed) + - raise the volume a bit + - play the announcement (from given url) + - wait for the player to finish playing + - restore the previous power and volume + - restore playback (if needed and if possible) + + This default implementation will only be used if the player + (provider) has no native support for the PLAY_ANNOUNCEMENT feature. + """ + prev_power = player.powered + prev_state = player.state + prev_synced_to = player.synced_to + queue = self.mass.player_queues.get(player.active_source) + prev_queue_active = queue and queue.active + prev_item_id = player.current_item_id + # unsync player if its currently synced + if prev_synced_to: + self.logger.debug( + "Announcement to player %s - unsyncing player...", + player.display_name, + ) + await self.cmd_unsync(player.player_id) + # stop player if its currently playing + elif prev_state in (PlayerState.PLAYING, PlayerState.PAUSED): + self.logger.debug( + "Announcement to player %s - stop existing content (%s)...", + player.display_name, + prev_item_id, + ) + await self.cmd_stop(player.player_id) + # wait for the player to stop + await self.wait_for_state(player, PlayerState.IDLE, 10, 0.4) + # adjust volume if needed + # in case of a (sync) group, we need to do this for all child players + prev_volumes: dict[str, int] = {} + async with TaskManager(self.mass) as tg: + for volume_player_id in player.group_childs or (player.player_id,): + if not (volume_player := self.get(volume_player_id)): + continue + # catch any players that have a different source active + if ( + volume_player.active_source + not in ( + player.active_source, + volume_player.player_id, + None, + ) + and volume_player.state == PlayerState.PLAYING + ): + self.logger.warning( + "Detected announcement to playergroup %s while group member %s is playing " + "other content, this may lead to unexpected behavior.", + player.display_name, + volume_player.display_name, + ) + tg.create_task(self.cmd_stop(volume_player.player_id)) + prev_volume = volume_player.volume_level + announcement_volume = self.get_announcement_volume(volume_player_id, volume_level) + temp_volume = announcement_volume or player.volume_level + if temp_volume != prev_volume: + prev_volumes[volume_player_id] = prev_volume + self.logger.debug( + "Announcement to player %s - setting temporary volume (%s)...", + volume_player.display_name, + announcement_volume, + ) + tg.create_task( + self.cmd_volume_set(volume_player.player_id, announcement_volume) + ) + # play the announcement + self.logger.debug( + "Announcement to player %s - playing the announcement on the player...", + player.display_name, + ) + await self.play_media(player_id=player.player_id, media=announcement) + # wait for the player(s) to play + await self.wait_for_state(player, PlayerState.PLAYING, 10, minimal_time=0.1) + # wait for the player to stop playing + if not announcement.duration: + media_info = await parse_tags(announcement.custom_data["url"]) + announcement.duration = media_info.duration or 60 + media_info.duration += 2 + await self.wait_for_state( + player, + PlayerState.IDLE, + max(announcement.duration * 2, 60), + announcement.duration + 2, + ) + self.logger.debug( + "Announcement to player %s - restore previous state...", player.display_name + ) + # restore volume + async with TaskManager(self.mass) as tg: + for volume_player_id, prev_volume in prev_volumes.items(): + tg.create_task(self.cmd_volume_set(volume_player_id, prev_volume)) + + await asyncio.sleep(0.2) + player.current_item_id = prev_item_id + # either power off the player or resume playing + if not prev_power: + await self.cmd_power(player.player_id, False) + return + elif prev_synced_to: + await self.cmd_sync(player.player_id, prev_synced_to) + elif prev_queue_active and prev_state == PlayerState.PLAYING: + await self.mass.player_queues.resume(queue.queue_id, True) + elif prev_state == PlayerState.PLAYING: + # player was playing something else - try to resume that here + self.logger.warning("Can not resume %s on %s", prev_item_id, player.display_name) + # TODO !! + + async def _poll_players(self) -> None: + """Background task that polls players for updates.""" + while True: + for player in list(self._players.values()): + player_id = player.player_id + # if the player is playing, update elapsed time every tick + # to ensure the queue has accurate details + player_playing = player.state == PlayerState.PLAYING + if player_playing: + self.mass.loop.call_soon(self.update, player_id) + # Poll player; + if not player.needs_poll: + continue + if (self.mass.loop.time() - player.last_poll) < player.poll_interval: + continue + player.last_poll = self.mass.loop.time() + if player_prov := self.get_player_provider(player_id): + try: + await player_prov.poll_player(player_id) + except PlayerUnavailableError: + player.available = False + player.state = PlayerState.IDLE + player.powered = False + except Exception as err: + self.logger.warning( + "Error while requesting latest state from player %s: %s", + player.display_name, + str(err), + exc_info=err if self.logger.isEnabledFor(10) else None, + ) + finally: + # always update player state + self.mass.loop.call_soon(self.update, player_id) + await asyncio.sleep(1) diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py new file mode 100644 index 00000000..b760b282 --- /dev/null +++ b/music_assistant/controllers/streams.py @@ -0,0 +1,980 @@ +""" +Controller to stream audio to players. + +The streams controller hosts a basic, unprotected HTTP-only webserver +purely to stream audio packets to players and some control endpoints such as +the upnp callbacks and json rpc api for slimproto clients. +""" + +from __future__ import annotations + +import os +import time +import urllib.parse +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING + +from aiofiles.os import wrap +from aiohttp import web +from music_assistant_models.config_entries import ( + CONF_ENTRY_ENABLE_ICY_METADATA, + ConfigEntry, + ConfigValueOption, + ConfigValueType, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + MediaType, + StreamType, + VolumeNormalizationMode, +) +from music_assistant_models.errors import QueueEmpty +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player_queue import PlayLogEntry + +from music_assistant.constants import ( + ANNOUNCE_ALERT_FILE, + CONF_BIND_IP, + CONF_BIND_PORT, + CONF_CROSSFADE, + CONF_CROSSFADE_DURATION, + CONF_HTTP_PROFILE, + CONF_OUTPUT_CHANNELS, + CONF_PUBLISH_IP, + CONF_SAMPLE_RATES, + CONF_VOLUME_NORMALIZATION, + CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO, + CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, + CONF_VOLUME_NORMALIZATION_RADIO, + CONF_VOLUME_NORMALIZATION_TRACKS, + MASS_LOGO_ONLINE, + SILENCE_FILE, + VERBOSE_LOG_LEVEL, +) +from music_assistant.helpers.audio import LOGGER as AUDIO_LOGGER +from music_assistant.helpers.audio import ( + check_audio_support, + crossfade_pcm_parts, + get_chunksize, + get_hls_substream, + get_icy_radio_stream, + get_media_stream, + get_player_filter_params, + get_silence, + get_stream_details, +) +from music_assistant.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER +from music_assistant.helpers.ffmpeg import get_ffmpeg_stream +from music_assistant.helpers.util import get_ip, get_ips, select_free_port, try_parse_bool +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.core_controller import CoreController + +if TYPE_CHECKING: + from music_assistant_models.config_entries import CoreConfig + from music_assistant_models.player import Player + from music_assistant_models.player_queue import PlayerQueue + from music_assistant_models.queue_item import QueueItem + from music_assistant_models.streamdetails import StreamDetails + + +DEFAULT_STREAM_HEADERS = { + "Server": "Music Assistant", + "transferMode.dlna.org": "Streaming", + "contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501 + "Cache-Control": "no-cache", + "Pragma": "no-cache", +} +ICY_HEADERS = { + "icy-name": "Music Assistant", + "icy-description": "Music Assistant - Your personal music assistant", + "icy-version": "1", + "icy-logo": MASS_LOGO_ONLINE, +} +FLOW_DEFAULT_SAMPLE_RATE = 48000 +FLOW_DEFAULT_BIT_DEPTH = 24 + + +isfile = wrap(os.path.isfile) + + +def parse_pcm_info(content_type: str) -> tuple[int, int, int]: + """Parse PCM info from a codec/content_type string.""" + params = ( + dict(urllib.parse.parse_qsl(content_type.replace(";", "&"))) if ";" in content_type else {} + ) + sample_rate = int(params.get("rate", 44100)) + sample_size = int(params.get("bitrate", 16)) + channels = int(params.get("channels", 2)) + return (sample_rate, sample_size, channels) + + +class StreamsController(CoreController): + """Webserver Controller to stream audio to players.""" + + domain: str = "streams" + + def __init__(self, *args, **kwargs) -> None: + """Initialize instance.""" + super().__init__(*args, **kwargs) + self._server = Webserver(self.logger, enable_dynamic_routes=True) + self.register_dynamic_route = self._server.register_dynamic_route + self.unregister_dynamic_route = self._server.unregister_dynamic_route + self.manifest.name = "Streamserver" + self.manifest.description = ( + "Music Assistant's core controller that is responsible for " + "streaming audio to players on the local network." + ) + self.manifest.icon = "cast-audio" + self.announcements: dict[str, str] = {} + + @property + def base_url(self) -> str: + """Return the base_url for the streamserver.""" + return self._server.base_url + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + default_ip = await get_ip() + all_ips = await get_ips() + default_port = await select_free_port(8097, 9200) + return ( + ConfigEntry( + key=CONF_BIND_PORT, + type=ConfigEntryType.INTEGER, + default_value=default_port, + label="TCP Port", + description="The TCP port to run the server. " + "Make sure that this server can be reached " + "on the given IP and TCP port by players on the local network.", + ), + ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_RADIO, + type=ConfigEntryType.STRING, + default_value=VolumeNormalizationMode.FALLBACK_DYNAMIC, + label="Volume normalization method for radio streams", + options=( + ConfigValueOption(x.value.replace("_", " ").title(), x.value) + for x in VolumeNormalizationMode + ), + category="audio", + ), + ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_TRACKS, + type=ConfigEntryType.STRING, + default_value=VolumeNormalizationMode.FALLBACK_DYNAMIC, + label="Volume normalization method for tracks", + options=( + ConfigValueOption(x.value.replace("_", " ").title(), x.value) + for x in VolumeNormalizationMode + ), + category="audio", + ), + ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO, + type=ConfigEntryType.FLOAT, + range=(-20, 10), + default_value=-6, + label="Fixed/fallback gain adjustment for radio streams", + category="audio", + ), + ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, + type=ConfigEntryType.FLOAT, + range=(-20, 10), + default_value=-6, + label="Fixed/fallback gain adjustment for tracks", + category="audio", + ), + ConfigEntry( + key=CONF_PUBLISH_IP, + type=ConfigEntryType.STRING, + default_value=default_ip, + label="Published IP address", + description="This IP address is communicated to players where to find this server. " + "Override the default in advanced scenarios, such as multi NIC configurations. \n" + "Make sure that this server can be reached " + "on the given IP and TCP port by players on the local network. \n" + "This is an advanced setting that should normally " + "not be adjusted in regular setups.", + category="advanced", + ), + ConfigEntry( + key=CONF_BIND_IP, + type=ConfigEntryType.STRING, + default_value="0.0.0.0", + options=(ConfigValueOption(x, x) for x in {"0.0.0.0", *all_ips}), + label="Bind to IP/interface", + description="Start the stream server on this specific interface. \n" + "Use 0.0.0.0 to bind to all interfaces, which is the default. \n" + "This is an advanced setting that should normally " + "not be adjusted in regular setups.", + category="advanced", + ), + ) + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + ffmpeg_present, libsoxr_support, version = await check_audio_support() + major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) + if not ffmpeg_present: + self.logger.error("FFmpeg binary not found on your system, playback will NOT work!.") + elif major_version < 6: + self.logger.error("FFMpeg version is too old, you may run into playback issues.") + elif not libsoxr_support: + self.logger.warning( + "FFmpeg version found without libsoxr support, " + "highest quality audio not available. " + ) + self.logger.info( + "Detected ffmpeg version %s %s", + version, + "with libsoxr support" if libsoxr_support else "", + ) + # copy log level to audio/ffmpeg loggers + AUDIO_LOGGER.setLevel(self.logger.level) + FFMPEG_LOGGER.setLevel(self.logger.level) + # start the webserver + self.publish_port = config.get_value(CONF_BIND_PORT) + self.publish_ip = config.get_value(CONF_PUBLISH_IP) + await self._server.setup( + bind_ip=config.get_value(CONF_BIND_IP), + bind_port=self.publish_port, + base_url=f"http://{self.publish_ip}:{self.publish_port}", + static_routes=[ + ( + "*", + "/flow/{queue_id}/{queue_item_id}.{fmt}", + self.serve_queue_flow_stream, + ), + ( + "*", + "/single/{queue_id}/{queue_item_id}.{fmt}", + self.serve_queue_item_stream, + ), + ( + "*", + "/command/{queue_id}/{command}.mp3", + self.serve_command_request, + ), + ( + "*", + "/announcement/{player_id}.{fmt}", + self.serve_announcement_stream, + ), + ], + ) + + async def close(self) -> None: + """Cleanup on exit.""" + await self._server.close() + + def resolve_stream_url( + self, + queue_item: QueueItem, + flow_mode: bool = False, + output_codec: ContentType = ContentType.FLAC, + ) -> str: + """Resolve the stream URL for the given QueueItem.""" + fmt = output_codec.value + # handle raw pcm without exact format specifiers + if output_codec.is_pcm() and ";" not in fmt: + fmt += f";codec=pcm;rate={44100};bitrate={16};channels={2}" + query_params = {} + base_path = "flow" if flow_mode else "single" + url = f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}" # noqa: E501 + # we add a timestamp as basic checksum + # most importantly this is to invalidate any caches + # but also to handle edge cases such as single track repeat + query_params["ts"] = str(int(time.time())) + url += "?" + urllib.parse.urlencode(query_params) + return url + + async def serve_queue_item_stream(self, request: web.Request) -> web.Response: + """Stream single queueitem audio to a player.""" + self._log_request(request) + queue_id = request.match_info["queue_id"] + queue = self.mass.player_queues.get(queue_id) + if not queue: + raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}") + queue_player = self.mass.players.get(queue_id) + queue_item_id = request.match_info["queue_item_id"] + queue_item = self.mass.player_queues.get_item(queue_id, queue_item_id) + if not queue_item: + raise web.HTTPNotFound(reason=f"Unknown Queue item: {queue_item_id}") + if not queue_item.streamdetails: + try: + queue_item.streamdetails = await get_stream_details( + mass=self.mass, queue_item=queue_item + ) + except Exception as e: + self.logger.error( + "Failed to get streamdetails for QueueItem %s: %s", queue_item_id, e + ) + raise web.HTTPNotFound(reason=f"No streamdetails for Queue item: {queue_item_id}") + # work out output format/details + output_format = await self._get_output_format( + output_format_str=request.match_info["fmt"], + player=queue_player, + default_sample_rate=queue_item.streamdetails.audio_format.sample_rate, + default_bit_depth=queue_item.streamdetails.audio_format.bit_depth, + ) + + # prepare request, add some DLNA/UPNP compatible headers + headers = { + **DEFAULT_STREAM_HEADERS, + "icy-name": queue_item.name, + } + resp = web.StreamResponse( + status=200, + reason="OK", + headers=headers, + ) + resp.content_type = f"audio/{output_format.output_format_str}" + http_profile: str = await self.mass.config.get_player_config_value( + queue_id, CONF_HTTP_PROFILE + ) + if http_profile == "forced_content_length" and queue_item.duration: + # guess content length based on duration + resp.content_length = get_chunksize(output_format, queue_item.duration) + elif http_profile == "chunked": + resp.enable_chunked_encoding() + + await resp.prepare(request) + + # return early if this is not a GET request + if request.method != "GET": + return resp + + # all checks passed, start streaming! + self.logger.debug( + "Start serving audio stream for QueueItem %s (%s) to %s", + queue_item.name, + queue_item.uri, + queue.display_name, + ) + self.mass.player_queues.track_loaded_in_buffer(queue_id, queue_item_id) + + # pick pcm format based on the streamdetails and player capabilities + if self.mass.config.get_raw_player_config_value(queue_id, CONF_VOLUME_NORMALIZATION, True): + # prefer f32 when volume normalization is enabled + bit_depth = 32 + floating_point = True + else: + bit_depth = queue_item.streamdetails.audio_format.bit_depth + floating_point = False + pcm_format = AudioFormat( + content_type=ContentType.from_bit_depth(bit_depth, floating_point), + sample_rate=queue_item.streamdetails.audio_format.sample_rate, + bit_depth=bit_depth, + channels=2, + ) + chunk_num = 0 + async for chunk in get_ffmpeg_stream( + audio_input=self.get_media_stream( + streamdetails=queue_item.streamdetails, + pcm_format=pcm_format, + ), + input_format=pcm_format, + output_format=output_format, + filter_params=get_player_filter_params(self.mass, queue_player.player_id), + # we don't allow the player to buffer too much ahead so we use readrate limiting + extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], + ): + try: + await resp.write(chunk) + chunk_num += 1 + except (BrokenPipeError, ConnectionResetError, ConnectionError): + break + if queue_item.streamdetails.stream_error: + self.logger.error( + "Error streaming QueueItem %s (%s) to %s", + queue_item.name, + queue_item.uri, + queue.display_name, + ) + return resp + + async def serve_queue_flow_stream(self, request: web.Request) -> web.Response: + """Stream Queue Flow audio to player.""" + self._log_request(request) + queue_id = request.match_info["queue_id"] + queue = self.mass.player_queues.get(queue_id) + if not queue: + raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}") + if not (queue_player := self.mass.players.get(queue_id)): + raise web.HTTPNotFound(reason=f"Unknown Player: {queue_id}") + start_queue_item_id = request.match_info["queue_item_id"] + start_queue_item = self.mass.player_queues.get_item(queue_id, start_queue_item_id) + if not start_queue_item: + raise web.HTTPNotFound(reason=f"Unknown Queue item: {start_queue_item_id}") + + # select the highest possible PCM settings for this player + flow_pcm_format = await self._select_flow_format(queue_player) + + # work out output format/details + output_format = await self._get_output_format( + output_format_str=request.match_info["fmt"], + player=queue_player, + default_sample_rate=flow_pcm_format.sample_rate, + default_bit_depth=flow_pcm_format.bit_depth, + ) + # work out ICY metadata support + icy_preference = self.mass.config.get_raw_player_config_value( + queue_id, + CONF_ENTRY_ENABLE_ICY_METADATA.key, + CONF_ENTRY_ENABLE_ICY_METADATA.default_value, + ) + enable_icy = request.headers.get("Icy-MetaData", "") == "1" and icy_preference != "disabled" + icy_meta_interval = 256000 if icy_preference == "full" else 16384 + + # prepare request, add some DLNA/UPNP compatible headers + headers = { + **DEFAULT_STREAM_HEADERS, + **ICY_HEADERS, + "Accept-Ranges": "none", + "Content-Type": f"audio/{output_format.output_format_str}", + } + if enable_icy: + headers["icy-metaint"] = str(icy_meta_interval) + + resp = web.StreamResponse( + status=200, + reason="OK", + headers=headers, + ) + http_profile: str = await self.mass.config.get_player_config_value( + queue_id, CONF_HTTP_PROFILE + ) + if http_profile == "forced_content_length": + # just set an insane high content length to make sure the player keeps playing + resp.content_length = get_chunksize(output_format, 12 * 3600) + elif http_profile == "chunked": + resp.enable_chunked_encoding() + + await resp.prepare(request) + + # return early if this is not a GET request + if request.method != "GET": + return resp + + # all checks passed, start streaming! + self.logger.debug("Start serving Queue flow audio stream for %s", queue.display_name) + + async for chunk in get_ffmpeg_stream( + audio_input=self.get_flow_stream( + queue=queue, start_queue_item=start_queue_item, pcm_format=flow_pcm_format + ), + input_format=flow_pcm_format, + output_format=output_format, + filter_params=get_player_filter_params(self.mass, queue_player.player_id), + chunk_size=icy_meta_interval if enable_icy else None, + # we don't allow the player to buffer too much ahead so we use readrate limiting + extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], + ): + try: + await resp.write(chunk) + except (BrokenPipeError, ConnectionResetError, ConnectionError): + # race condition + break + + if not enable_icy: + continue + + # if icy metadata is enabled, send the icy metadata after the chunk + if ( + # use current item here and not buffered item, otherwise + # the icy metadata will be too much ahead + (current_item := queue.current_item) + and current_item.streamdetails + and current_item.streamdetails.stream_title + ): + title = current_item.streamdetails.stream_title + elif queue and current_item and current_item.name: + title = current_item.name + else: + title = "Music Assistant" + metadata = f"StreamTitle='{title}';".encode() + if icy_preference == "full" and current_item and current_item.image: + metadata += f"StreamURL='{current_item.image.path}'".encode() + while len(metadata) % 16 != 0: + metadata += b"\x00" + length = len(metadata) + length_b = chr(int(length / 16)).encode() + await resp.write(length_b + metadata) + + return resp + + async def serve_command_request(self, request: web.Request) -> web.Response: + """Handle special 'command' request for a player.""" + self._log_request(request) + queue_id = request.match_info["queue_id"] + command = request.match_info["command"] + if command == "next": + self.mass.create_task(self.mass.player_queues.next(queue_id)) + return web.FileResponse(SILENCE_FILE, headers={"icy-name": "Music Assistant"}) + + async def serve_announcement_stream(self, request: web.Request) -> web.Response: + """Stream announcement audio to a player.""" + self._log_request(request) + player_id = request.match_info["player_id"] + player = self.mass.player_queues.get(player_id) + if not player: + raise web.HTTPNotFound(reason=f"Unknown Player: {player_id}") + if player_id not in self.announcements: + raise web.HTTPNotFound(reason=f"No pending announcements for Player: {player_id}") + announcement_url = self.announcements[player_id] + use_pre_announce = try_parse_bool(request.query.get("pre_announce")) + + # work out output format/details + fmt = request.match_info.get("fmt", announcement_url.rsplit(".")[-1]) + audio_format = AudioFormat(content_type=ContentType.try_parse(fmt)) + + http_profile: str = await self.mass.config.get_player_config_value( + player_id, CONF_HTTP_PROFILE + ) + if http_profile == "forced_content_length": + # given the fact that an announcement is just a short audio clip, + # just send it over completely at once so we have a fixed content length + data = b"" + async for chunk in self.get_announcement_stream( + announcement_url=announcement_url, + output_format=audio_format, + use_pre_announce=use_pre_announce, + ): + data += chunk + return web.Response( + body=data, + content_type=f"audio/{audio_format.output_format_str}", + headers=DEFAULT_STREAM_HEADERS, + ) + + resp = web.StreamResponse( + status=200, + reason="OK", + headers=DEFAULT_STREAM_HEADERS, + ) + resp.content_type = f"audio/{audio_format.output_format_str}" + if http_profile == "chunked": + resp.enable_chunked_encoding() + + await resp.prepare(request) + + # return early if this is not a GET request + if request.method != "GET": + return resp + + # all checks passed, start streaming! + self.logger.debug( + "Start serving audio stream for Announcement %s to %s", + announcement_url, + player.display_name, + ) + async for chunk in self.get_announcement_stream( + announcement_url=announcement_url, + output_format=audio_format, + use_pre_announce=use_pre_announce, + ): + try: + await resp.write(chunk) + except (BrokenPipeError, ConnectionResetError): + break + + self.logger.debug( + "Finished serving audio stream for Announcement %s to %s", + announcement_url, + player.display_name, + ) + + return resp + + def get_command_url(self, player_or_queue_id: str, command: str) -> str: + """Get the url for the special command stream.""" + return f"{self.base_url}/command/{player_or_queue_id}/{command}.mp3" + + def get_announcement_url( + self, + player_id: str, + announcement_url: str, + use_pre_announce: bool = False, + content_type: ContentType = ContentType.MP3, + ) -> str: + """Get the url for the special announcement stream.""" + self.announcements[player_id] = announcement_url + # use stream server to host announcement on local network + # this ensures playback on all players, including ones that do not + # like https hosts and it also offers the pre-announce 'bell' + return f"{self.base_url}/announcement/{player_id}.{content_type.value}?pre_announce={use_pre_announce}" # noqa: E501 + + async def get_flow_stream( + self, + queue: PlayerQueue, + start_queue_item: QueueItem, + pcm_format: AudioFormat, + ) -> AsyncGenerator[bytes, None]: + """Get a flow stream of all tracks in the queue as raw PCM audio.""" + # ruff: noqa: PLR0915 + assert pcm_format.content_type.is_pcm() + queue_track = None + last_fadeout_part = b"" + queue.flow_mode = True + use_crossfade = await self.mass.config.get_player_config_value( + queue.queue_id, CONF_CROSSFADE + ) + if not start_queue_item: + # this can happen in some (edge case) race conditions + return + if start_queue_item.media_type != MediaType.TRACK: + use_crossfade = False + pcm_sample_size = int( + pcm_format.sample_rate * (pcm_format.bit_depth / 8) * pcm_format.channels + ) + self.logger.info( + "Start Queue Flow stream for Queue %s - crossfade: %s", + queue.display_name, + use_crossfade, + ) + total_bytes_sent = 0 + + while True: + # get (next) queue item to stream + if queue_track is None: + queue_track = start_queue_item + else: + try: + queue_track = await self.mass.player_queues.load_next_item(queue.queue_id) + except QueueEmpty: + break + + if queue_track.streamdetails is None: + raise RuntimeError( + "No Streamdetails known for queue item %s", queue_track.queue_item_id + ) + + self.logger.debug( + "Start Streaming queue track: %s (%s) for queue %s", + queue_track.streamdetails.uri, + queue_track.name, + queue.display_name, + ) + self.mass.player_queues.track_loaded_in_buffer( + queue.queue_id, queue_track.queue_item_id + ) + # append to play log so the queue controller can work out which track is playing + play_log_entry = PlayLogEntry(queue_track.queue_item_id) + queue.flow_mode_stream_log.append(play_log_entry) + + # set some basic vars + pcm_sample_size = int(pcm_format.sample_rate * (pcm_format.bit_depth / 8) * 2) + crossfade_duration = self.mass.config.get_raw_player_config_value( + queue.queue_id, CONF_CROSSFADE_DURATION, 10 + ) + crossfade_size = int(pcm_sample_size * crossfade_duration) + bytes_written = 0 + buffer = b"" + # handle incoming audio chunks + async for chunk in self.get_media_stream( + queue_track.streamdetails, + pcm_format=pcm_format, + ): + # buffer size needs to be big enough to include the crossfade part + req_buffer_size = pcm_sample_size * 2 if not use_crossfade else crossfade_size + + # ALWAYS APPEND CHUNK TO BUFFER + buffer += chunk + del chunk + if len(buffer) < req_buffer_size: + # buffer is not full enough, move on + continue + + #### HANDLE CROSSFADE OF PREVIOUS TRACK AND NEW TRACK + if last_fadeout_part: + # perform crossfade + fadein_part = buffer[:crossfade_size] + remaining_bytes = buffer[crossfade_size:] + crossfade_part = await crossfade_pcm_parts( + fadein_part, + last_fadeout_part, + pcm_format=pcm_format, + ) + # send crossfade_part (as one big chunk) + bytes_written += len(crossfade_part) + yield crossfade_part + + # also write the leftover bytes from the crossfade action + if remaining_bytes: + yield remaining_bytes + bytes_written += len(remaining_bytes) + del remaining_bytes + # clear vars + last_fadeout_part = b"" + buffer = b"" + + #### OTHER: enough data in buffer, feed to output + while len(buffer) > req_buffer_size: + yield buffer[:pcm_sample_size] + bytes_written += pcm_sample_size + buffer = buffer[pcm_sample_size:] + + #### HANDLE END OF TRACK + if last_fadeout_part: + # edge case: we did not get enough data to make the crossfade + yield last_fadeout_part + bytes_written += len(last_fadeout_part) + last_fadeout_part = b"" + if use_crossfade: + # if crossfade is enabled, save fadeout part to pickup for next track + last_fadeout_part = buffer[-crossfade_size:] + remaining_bytes = buffer[:-crossfade_size] + if remaining_bytes: + yield remaining_bytes + bytes_written += len(remaining_bytes) + del remaining_bytes + elif buffer: + # no crossfade enabled, just yield the buffer last part + bytes_written += len(buffer) + yield buffer + # make sure the buffer gets cleaned up + del buffer + + # update duration details based on the actual pcm data we sent + # this also accounts for crossfade and silence stripping + seconds_streamed = bytes_written / pcm_sample_size + queue_track.streamdetails.seconds_streamed = seconds_streamed + queue_track.streamdetails.duration = ( + queue_track.streamdetails.seek_position + seconds_streamed + ) + play_log_entry.seconds_streamed = seconds_streamed + play_log_entry.duration = queue_track.streamdetails.duration + total_bytes_sent += bytes_written + self.logger.debug( + "Finished Streaming queue track: %s (%s) on queue %s", + queue_track.streamdetails.uri, + queue_track.name, + queue.display_name, + ) + #### HANDLE END OF QUEUE FLOW STREAM + # end of queue flow: make sure we yield the last_fadeout_part + if last_fadeout_part: + yield last_fadeout_part + # correct seconds streamed/duration + last_part_seconds = len(last_fadeout_part) / pcm_sample_size + queue_track.streamdetails.seconds_streamed += last_part_seconds + queue_track.streamdetails.duration += last_part_seconds + del last_fadeout_part + total_bytes_sent += bytes_written + self.logger.info("Finished Queue Flow stream for Queue %s", queue.display_name) + + async def get_announcement_stream( + self, announcement_url: str, output_format: AudioFormat, use_pre_announce: bool = False + ) -> AsyncGenerator[bytes, None]: + """Get the special announcement stream.""" + # work out output format/details + fmt = announcement_url.rsplit(".")[-1] + audio_format = AudioFormat(content_type=ContentType.try_parse(fmt)) + extra_args = [] + filter_params = ["loudnorm=I=-10:LRA=11:TP=-2"] + if use_pre_announce: + extra_args += [ + "-i", + ANNOUNCE_ALERT_FILE, + "-filter_complex", + "[1:a][0:a]concat=n=2:v=0:a=1,loudnorm=I=-10:LRA=11:TP=-1.5", + ] + filter_params = [] + async for chunk in get_ffmpeg_stream( + audio_input=announcement_url, + input_format=audio_format, + output_format=output_format, + extra_args=extra_args, + filter_params=filter_params, + ): + yield chunk + + async def get_media_stream( + self, + streamdetails: StreamDetails, + pcm_format: AudioFormat, + ) -> AsyncGenerator[tuple[bool, bytes], None]: + """Get the audio stream for the given streamdetails as raw pcm chunks.""" + is_radio = streamdetails.media_type == MediaType.RADIO or not streamdetails.duration + if is_radio: + streamdetails.seek_position = 0 + # collect all arguments for ffmpeg + filter_params = [] + extra_input_args = [] + # handle volume normalization + enable_volume_normalization = ( + streamdetails.target_loudness is not None + and streamdetails.volume_normalization_mode != VolumeNormalizationMode.DISABLED + ) + dynamic_volume_normalization = ( + streamdetails.volume_normalization_mode == VolumeNormalizationMode.DYNAMIC + and enable_volume_normalization + ) + if dynamic_volume_normalization: + # volume normalization using loudnorm filter (in dynamic mode) + # which also collects the measurement on the fly during playback + # more info: https://k.ylo.ph/2016/04/04/loudnorm.html + filter_rule = f"loudnorm=I={streamdetails.target_loudness}:TP=-2.0:LRA=10.0:offset=0.0" + filter_rule += ":print_format=json" + filter_params.append(filter_rule) + elif ( + enable_volume_normalization + and streamdetails.volume_normalization_mode == VolumeNormalizationMode.FIXED_GAIN + ): + # apply used defined fixed volume/gain correction + gain_correct: float = await self.mass.config.get_core_config_value( + CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO + if streamdetails.media_type == MediaType.RADIO + else CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, + ) + gain_correct = round(gain_correct, 2) + filter_params.append(f"volume={gain_correct}dB") + elif enable_volume_normalization and streamdetails.loudness is not None: + # volume normalization with known loudness measurement + # apply volume/gain correction + gain_correct = streamdetails.target_loudness - streamdetails.loudness + gain_correct = round(gain_correct, 2) + filter_params.append(f"volume={gain_correct}dB") + + # work out audio source for these streamdetails + if streamdetails.stream_type == StreamType.CUSTOM: + audio_source = self.mass.get_provider(streamdetails.provider).get_audio_stream( + streamdetails, + seek_position=streamdetails.seek_position, + ) + elif streamdetails.stream_type == StreamType.ICY: + audio_source = get_icy_radio_stream(self.mass, streamdetails.path, streamdetails) + elif streamdetails.stream_type == StreamType.HLS: + substream = await get_hls_substream(self.mass, streamdetails.path) + audio_source = substream.path + if streamdetails.media_type == MediaType.RADIO: + # Especially the BBC streams struggle when they're played directly + # with ffmpeg, where they just stop after some minutes, + # so we tell ffmpeg to loop around in this case. + extra_input_args += ["-stream_loop", "-1", "-re"] + else: + audio_source = streamdetails.path + + # add support for decryption key provided in streamdetails + if streamdetails.decryption_key: + extra_input_args += ["-decryption_key", streamdetails.decryption_key] + + # handle seek support + if ( + streamdetails.seek_position + and streamdetails.media_type != MediaType.RADIO + and streamdetails.stream_type != StreamType.CUSTOM + ): + extra_input_args += ["-ss", str(int(streamdetails.seek_position))] + + if streamdetails.media_type == MediaType.RADIO: + # pad some silence before the radio stream starts to create some headroom + # for radio stations that do not provide any look ahead buffer + # without this, some radio streams jitter a lot, especially with dynamic normalization + pad_seconds = 5 if dynamic_volume_normalization else 2 + async for chunk in get_silence(pad_seconds, pcm_format): + yield chunk + + async for chunk in get_media_stream( + self.mass, + streamdetails=streamdetails, + pcm_format=pcm_format, + audio_source=audio_source, + filter_params=filter_params, + extra_input_args=extra_input_args, + ): + yield chunk + + def _log_request(self, request: web.Request) -> None: + """Log request.""" + if not self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + return + self.logger.log( + VERBOSE_LOG_LEVEL, + "Got %s request to %s from %s\nheaders: %s\n", + request.method, + request.path, + request.remote, + request.headers, + ) + + async def _get_output_format( + self, + output_format_str: str, + player: Player, + default_sample_rate: int, + default_bit_depth: int, + ) -> AudioFormat: + """Parse (player specific) output format details for given format string.""" + content_type: ContentType = ContentType.try_parse(output_format_str) + supported_rates_conf = await self.mass.config.get_player_config_value( + player.player_id, CONF_SAMPLE_RATES + ) + supported_sample_rates: tuple[int] = tuple(x[0] for x in supported_rates_conf) + supported_bit_depths: tuple[int] = tuple(x[1] for x in supported_rates_conf) + player_max_bit_depth = max(supported_bit_depths) + if content_type.is_pcm() or content_type == ContentType.WAV: + # parse pcm details from format string + output_sample_rate, output_bit_depth, output_channels = parse_pcm_info( + output_format_str + ) + if content_type == ContentType.PCM: + # resolve generic pcm type + content_type = ContentType.from_bit_depth(output_bit_depth) + else: + if default_sample_rate in supported_sample_rates: + output_sample_rate = default_sample_rate + else: + output_sample_rate = max(supported_sample_rates) + output_bit_depth = min(default_bit_depth, player_max_bit_depth) + output_channels_str = self.mass.config.get_raw_player_config_value( + player.player_id, CONF_OUTPUT_CHANNELS, "stereo" + ) + output_channels = 1 if output_channels_str != "stereo" else 2 + if not content_type.is_lossless(): + output_bit_depth = 16 + output_sample_rate = min(48000, output_sample_rate) + return AudioFormat( + content_type=content_type, + sample_rate=output_sample_rate, + bit_depth=output_bit_depth, + channels=output_channels, + output_format_str=output_format_str, + ) + + async def _select_flow_format( + self, + player: Player, + ) -> AudioFormat: + """Parse (player specific) flow stream PCM format.""" + supported_rates_conf = await self.mass.config.get_player_config_value( + player.player_id, CONF_SAMPLE_RATES + ) + supported_sample_rates: tuple[int] = tuple(x[0] for x in supported_rates_conf) + supported_bit_depths: tuple[int] = tuple(x[1] for x in supported_rates_conf) + player_max_bit_depth = max(supported_bit_depths) + for sample_rate in (192000, 96000, 48000, 44100): + if sample_rate in supported_sample_rates: + output_sample_rate = sample_rate + break + if self.mass.config.get_raw_player_config_value( + player.player_id, CONF_VOLUME_NORMALIZATION, True + ): + # prefer f32 when volume normalization is enabled + output_bit_depth = 32 + floating_point = True + else: + output_bit_depth = min(24, player_max_bit_depth) + floating_point = False + return AudioFormat( + content_type=ContentType.from_bit_depth(output_bit_depth, floating_point), + sample_rate=output_sample_rate, + bit_depth=output_bit_depth, + channels=2, + ) diff --git a/music_assistant/controllers/webserver.py b/music_assistant/controllers/webserver.py new file mode 100644 index 00000000..29cfe9d0 --- /dev/null +++ b/music_assistant/controllers/webserver.py @@ -0,0 +1,408 @@ +""" +Controller that manages the builtin webserver that hosts the api and frontend. + +Unlike the streamserver (which is as simple and unprotected as possible), +this webserver allows for more fine grained configuration to better secure it. +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import urllib.parse +from concurrent import futures +from contextlib import suppress +from functools import partial +from typing import TYPE_CHECKING, Any, Final + +from aiohttp import WSMsgType, web +from music_assistant_frontend import where as locate_frontend +from music_assistant_models.api import ( + CommandMessage, + ErrorResultMessage, + MessageType, + SuccessResultMessage, +) +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import InvalidCommand + +from music_assistant.constants import CONF_BIND_IP, CONF_BIND_PORT, VERBOSE_LOG_LEVEL +from music_assistant.helpers.api import APICommandHandler, parse_arguments +from music_assistant.helpers.audio import get_preview_stream +from music_assistant.helpers.util import get_ip, get_ips +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.core_controller import CoreController + +if TYPE_CHECKING: + from collections.abc import Awaitable + + from music_assistant_models.config_entries import ConfigValueType, CoreConfig + from music_assistant_models.event import MassEvent + +DEFAULT_SERVER_PORT = 8095 +CONF_BASE_URL = "base_url" +CONF_EXPOSE_SERVER = "expose_server" +MAX_PENDING_MSG = 512 +CANCELLATION_ERRORS: Final = (asyncio.CancelledError, futures.CancelledError) + + +class WebserverController(CoreController): + """Core Controller that manages the builtin webserver that hosts the api and frontend.""" + + domain: str = "webserver" + + def __init__(self, *args, **kwargs) -> None: + """Initialize instance.""" + super().__init__(*args, **kwargs) + self._server = Webserver(self.logger, enable_dynamic_routes=False) + self.clients: set[WebsocketClientHandler] = set() + self.manifest.name = "Web Server (frontend and api)" + self.manifest.description = ( + "The built-in webserver that hosts the Music Assistant Websockets API and frontend" + ) + self.manifest.icon = "web-box" + + @property + def base_url(self) -> str: + """Return the base_url for the streamserver.""" + return self._server.base_url + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + default_publish_ip = await get_ip() + if self.mass.running_as_hass_addon: + return ( + ConfigEntry( + key=CONF_EXPOSE_SERVER, + type=ConfigEntryType.BOOLEAN, + # hardcoded/static value + default_value=False, + label="Expose the webserver (port 8095)", + description="By default the Music Assistant webserver " + "(serving the API and frontend), runs on a protected internal network only " + "and you can securely access the webinterface using " + "Home Assistant's ingress service from the sidebar menu.\n\n" + "By enabling this option you also allow direct access to the webserver " + "from your local network, meaning you can navigate to " + f"http://{default_publish_ip}:8095 to access the webinterface. \n\n" + "Use this option on your own risk and never expose this port " + "directly to the internet.", + ), + ) + + # HA supervisor not present: user is responsible for securing the webserver + # we give the tools to do so by presenting config options + all_ips = await get_ips() + default_base_url = f"http://{default_publish_ip}:{DEFAULT_SERVER_PORT}" + return ( + ConfigEntry( + key=CONF_BASE_URL, + type=ConfigEntryType.STRING, + default_value=default_base_url, + label="Base URL", + description="The (base) URL to reach this webserver in the network. \n" + "Override this in advanced scenarios where for example you're running " + "the webserver behind a reverse proxy.", + ), + ConfigEntry( + key=CONF_BIND_PORT, + type=ConfigEntryType.INTEGER, + default_value=DEFAULT_SERVER_PORT, + label="TCP Port", + description="The TCP port to run the webserver.", + ), + ConfigEntry( + key=CONF_BIND_IP, + type=ConfigEntryType.STRING, + default_value="0.0.0.0", + options=(ConfigValueOption(x, x) for x in {"0.0.0.0", *all_ips}), + label="Bind to IP/interface", + description="Start the (web)server on this specific interface. \n" + "Use 0.0.0.0 to bind to all interfaces. \n" + "Set this address for example to a docker-internal network, " + "to enhance security and protect outside access to the webinterface and API. \n\n" + "This is an advanced setting that should normally " + "not be adjusted in regular setups.", + category="advanced", + ), + ) + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + # work out all routes + routes: list[tuple[str, str, Awaitable]] = [] + # frontend routes + frontend_dir = locate_frontend() + for filename in next(os.walk(frontend_dir))[2]: + if filename.endswith(".py"): + continue + filepath = os.path.join(frontend_dir, filename) + handler = partial(self._server.serve_static, filepath) + routes.append(("GET", f"/{filename}", handler)) + # add index + index_path = os.path.join(frontend_dir, "index.html") + handler = partial(self._server.serve_static, index_path) + routes.append(("GET", "/", handler)) + # add info + routes.append(("GET", "/info", self._handle_server_info)) + # add logging + routes.append(("GET", "/music-assistant.log", self._handle_application_log)) + # add websocket api + routes.append(("GET", "/ws", self._handle_ws_client)) + # also host the image proxy on the webserver + routes.append(("GET", "/imageproxy", self.mass.metadata.handle_imageproxy)) + # also host the audio preview service + routes.append(("GET", "/preview", self.serve_preview_stream)) + # start the webserver + default_publish_ip = await get_ip() + if self.mass.running_as_hass_addon: + # if we're running on the HA supervisor the webserver is secured by HA ingress + # we only start the webserver on the internal docker network and ingress connects + # to that internally and exposes the webUI securely + # if a user also wants to expose a the webserver non securely on his internal + # network he/she should explicitly do so (and know the risks) + self.publish_port = DEFAULT_SERVER_PORT + if config.get_value(CONF_EXPOSE_SERVER): + bind_ip = "0.0.0.0" + self.publish_ip = default_publish_ip + else: + # use internal ("172.30.32.) IP + self.publish_ip = bind_ip = next( + (x for x in await get_ips() if x.startswith("172.30.32.")), default_publish_ip + ) + base_url = f"http://{self.publish_ip}:{self.publish_port}" + else: + base_url = config.get_value(CONF_BASE_URL) + self.publish_port = config.get_value(CONF_BIND_PORT) + self.publish_ip = default_publish_ip + bind_ip = config.get_value(CONF_BIND_IP) + await self._server.setup( + bind_ip=bind_ip, + bind_port=self.publish_port, + base_url=base_url, + static_routes=routes, + # add assets subdir as static_content + static_content=("/assets", os.path.join(frontend_dir, "assets"), "assets"), + ) + + async def close(self) -> None: + """Cleanup on exit.""" + for client in set(self.clients): + await client.disconnect() + await self._server.close() + + async def serve_preview_stream(self, request: web.Request): + """Serve short preview sample.""" + provider_instance_id_or_domain = request.query["provider"] + item_id = urllib.parse.unquote(request.query["item_id"]) + resp = web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "audio/aac"}) + await resp.prepare(request) + async for chunk in get_preview_stream(self.mass, provider_instance_id_or_domain, item_id): + await resp.write(chunk) + return resp + + async def _handle_server_info(self, request: web.Request) -> web.Response: + """Handle request for server info.""" + return web.json_response(self.mass.get_server_info().to_dict()) + + async def _handle_ws_client(self, request: web.Request) -> web.WebSocketResponse: + connection = WebsocketClientHandler(self, request) + if lang := request.headers.get("Accept-Language"): + self.mass.metadata.set_default_preferred_language(lang.split(",")[0]) + try: + self.clients.add(connection) + return await connection.handle_client() + finally: + self.clients.remove(connection) + + async def _handle_application_log(self, request: web.Request) -> web.Response: + """Handle request to get the application log.""" + log_data = await self.mass.get_application_log() + return web.Response(text=log_data, content_type="text/text") + + +class WebsocketClientHandler: + """Handle an active websocket client connection.""" + + def __init__(self, webserver: WebserverController, request: web.Request) -> None: + """Initialize an active connection.""" + self.mass = webserver.mass + self.request = request + self.wsock = web.WebSocketResponse(heartbeat=55) + self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) + self._handle_task: asyncio.Task | None = None + self._writer_task: asyncio.Task | None = None + self._logger = webserver.logger + + async def disconnect(self) -> None: + """Disconnect client.""" + self._cancel() + if self._writer_task is not None: + await self._writer_task + + async def handle_client(self) -> web.WebSocketResponse: + """Handle a websocket response.""" + # ruff: noqa: PLR0915 + request = self.request + wsock = self.wsock + try: + async with asyncio.timeout(10): + await wsock.prepare(request) + except TimeoutError: + self._logger.warning("Timeout preparing request from %s", request.remote) + return wsock + + self._logger.log(VERBOSE_LOG_LEVEL, "Connection from %s", request.remote) + self._handle_task = asyncio.current_task() + self._writer_task = asyncio.create_task(self._writer()) + + # send server(version) info when client connects + self._send_message(self.mass.get_server_info()) + + # forward all events to clients + def handle_event(event: MassEvent) -> None: + self._send_message(event) + + unsub_callback = self.mass.subscribe(handle_event) + + disconnect_warn = None + + try: + while not wsock.closed: + msg = await wsock.receive() + + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): + break + + if msg.type != WSMsgType.TEXT: + disconnect_warn = "Received non-Text message." + break + + self._logger.log(VERBOSE_LOG_LEVEL, "Received: %s", msg.data) + + try: + command_msg = CommandMessage.from_json(msg.data) + except ValueError: + disconnect_warn = f"Received invalid JSON: {msg.data}" + break + + self._handle_command(command_msg) + + except asyncio.CancelledError: + self._logger.debug("Connection closed by client") + + except Exception: + self._logger.exception("Unexpected error inside websocket API") + + finally: + # Handle connection shutting down. + unsub_callback() + self._logger.log(VERBOSE_LOG_LEVEL, "Unsubscribed from events") + + try: + self._to_write.put_nowait(None) + # Make sure all error messages are written before closing + await self._writer_task + await wsock.close() + except asyncio.QueueFull: # can be raised by put_nowait + self._writer_task.cancel() + + finally: + if disconnect_warn is None: + self._logger.log(VERBOSE_LOG_LEVEL, "Disconnected") + else: + self._logger.warning("Disconnected: %s", disconnect_warn) + + return wsock + + def _handle_command(self, msg: CommandMessage) -> None: + """Handle an incoming command from the client.""" + self._logger.debug("Handling command %s", msg.command) + + # work out handler for the given path/command + handler = self.mass.command_handlers.get(msg.command) + + if handler is None: + self._send_message( + ErrorResultMessage( + msg.message_id, + InvalidCommand.error_code, + f"Invalid command: {msg.command}", + ) + ) + self._logger.warning("Invalid command: %s", msg.command) + return + + # schedule task to handle the command + asyncio.create_task(self._run_handler(handler, msg)) + + async def _run_handler(self, handler: APICommandHandler, msg: CommandMessage) -> None: + try: + args = parse_arguments(handler.signature, handler.type_hints, msg.args) + result = handler.target(**args) + if hasattr(result, "__anext__"): + # handle async generator (for really large listings) + iterator = result + result: list[Any] = [] + async for item in iterator: + result.append(item) + if len(result) >= 500: + self._send_message( + SuccessResultMessage(msg.message_id, result, partial=True) + ) + result = [] + elif asyncio.iscoroutine(result): + result = await result + self._send_message(SuccessResultMessage(msg.message_id, result)) + except Exception as err: + if self._logger.isEnabledFor(logging.DEBUG): + self._logger.exception("Error handling message: %s", msg) + else: + self._logger.error("Error handling message: %s: %s", msg.command, str(err)) + self._send_message( + ErrorResultMessage(msg.message_id, getattr(err, "error_code", 999), str(err)) + ) + + async def _writer(self) -> None: + """Write outgoing messages.""" + # Exceptions if Socket disconnected or cancelled by connection handler + with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): + while not self.wsock.closed: + if (process := await self._to_write.get()) is None: + break + + if not isinstance(process, str): + message: str = process() + else: + message = process + self._logger.log(VERBOSE_LOG_LEVEL, "Writing: %s", message) + await self.wsock.send_str(message) + + def _send_message(self, message: MessageType) -> None: + """Send a message to the client. + + Closes connection if the client is not reading the messages. + + Async friendly. + """ + _message = message.to_json() + + try: + self._to_write.put_nowait(_message) + except asyncio.QueueFull: + self._logger.error("Client exceeded max pending messages: %s", MAX_PENDING_MSG) + + self._cancel() + + def _cancel(self) -> None: + """Cancel the connection.""" + if self._handle_task is not None: + self._handle_task.cancel() + if self._writer_task is not None: + self._writer_task.cancel() diff --git a/music_assistant/helpers/__init__.py b/music_assistant/helpers/__init__.py new file mode 100644 index 00000000..ca681394 --- /dev/null +++ b/music_assistant/helpers/__init__.py @@ -0,0 +1 @@ +"""Various server-seecific utils/helpers.""" diff --git a/music_assistant/helpers/api.py b/music_assistant/helpers/api.py new file mode 100644 index 00000000..ddc7f0cd --- /dev/null +++ b/music_assistant/helpers/api.py @@ -0,0 +1,167 @@ +"""Helpers for dealing with API's to interact with Music Assistant.""" + +from __future__ import annotations + +import inspect +import logging +from collections.abc import Callable, Coroutine +from dataclasses import MISSING, dataclass +from datetime import datetime +from enum import Enum +from types import NoneType, UnionType +from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints + +LOGGER = logging.getLogger(__name__) + +_F = TypeVar("_F", bound=Callable[..., Any]) + + +@dataclass +class APICommandHandler: + """Model for an API command handler.""" + + command: str + signature: inspect.Signature + type_hints: dict[str, Any] + target: Callable[..., Coroutine[Any, Any, Any]] + + @classmethod + def parse( + cls, command: str, func: Callable[..., Coroutine[Any, Any, Any]] + ) -> APICommandHandler: + """Parse APICommandHandler by providing a function.""" + type_hints = get_type_hints(func) + # workaround for generic typevar ItemCls that needs to be resolved + # to the real media item type. TODO: find a better way to do this + # without this hack + for key, value in type_hints.items(): + if not hasattr(value, "__name__"): + continue + if value.__name__ == "ItemCls": + type_hints[key] = func.__self__.item_cls + return APICommandHandler( + command=command, + signature=inspect.signature(func), + type_hints=type_hints, + target=func, + ) + + +def api_command(command: str) -> Callable[[_F], _F]: + """Decorate a function as API route/command.""" + + def decorate(func: _F) -> _F: + func.api_cmd = command # type: ignore[attr-defined] + return func + + return decorate + + +def parse_arguments( + func_sig: inspect.Signature, + func_types: dict[str, Any], + args: dict | None, + strict: bool = False, +) -> dict[str, Any]: + """Parse (and convert) incoming arguments to correct types.""" + if args is None: + args = {} + final_args = {} + # ignore extra args if not strict + if strict: + for key, value in args.items(): + if key not in func_sig.parameters: + raise KeyError(f"Invalid parameter: '{key}'") + # parse arguments to correct type + for name, param in func_sig.parameters.items(): + value = args.get(name) + default = MISSING if param.default is inspect.Parameter.empty else param.default + final_args[name] = parse_value(name, value, func_types[name], default) + return final_args + + +def parse_utc_timestamp(datetime_string: str) -> datetime: + """Parse datetime from string.""" + return datetime.fromisoformat(datetime_string.replace("Z", "+00:00")) + + +def parse_value(name: str, value: Any, value_type: Any, default: Any = MISSING) -> Any: + """Try to parse a value from raw (json) data and type annotations.""" + if isinstance(value, dict) and hasattr(value_type, "from_dict"): + if "media_type" in value and value["media_type"] != value_type.media_type: + msg = "Invalid MediaType" + raise ValueError(msg) + return value_type.from_dict(value) + + if value is None and not isinstance(default, type(MISSING)): + return default + if value is None and value_type is NoneType: + return None + origin = get_origin(value_type) + if origin in (tuple, list): + return origin( + parse_value(name, subvalue, get_args(value_type)[0]) + for subvalue in value + if subvalue is not None + ) + if origin is dict: + subkey_type = get_args(value_type)[0] + subvalue_type = get_args(value_type)[1] + return { + parse_value(subkey, subkey, subkey_type): parse_value( + f"{subkey}.value", subvalue, subvalue_type + ) + for subkey, subvalue in value.items() + } + if origin is Union or origin is UnionType: + # try all possible types + sub_value_types = get_args(value_type) + for sub_arg_type in sub_value_types: + if value is NoneType and sub_arg_type is NoneType: + return value + # try them all until one succeeds + try: + return parse_value(name, value, sub_arg_type) + except (KeyError, TypeError, ValueError): + pass + # if we get to this point, all possibilities failed + # find out if we should raise or log this + err = ( + f"Value {value} of type {type(value)} is invalid for {name}, " + f"expected value of type {value_type}" + ) + if NoneType not in sub_value_types: + # raise exception, we have no idea how to handle this value + raise TypeError(err) + # failed to parse the (sub) value but None allowed, log only + logging.getLogger(__name__).warning(err) + return None + if origin is type: + return eval(value) + if value_type is Any: + return value + if value is None and value_type is not NoneType: + msg = f"`{name}` of type `{value_type}` is required." + raise KeyError(msg) + + try: + if issubclass(value_type, Enum): # type: ignore[arg-type] + return value_type(value) # type: ignore[operator] + if issubclass(value_type, datetime): # type: ignore[arg-type] + return parse_utc_timestamp(value) + except TypeError: + # happens if value_type is not a class + pass + + if value_type is float and isinstance(value, int): + return float(value) + if value_type is int and isinstance(value, str) and value.isnumeric(): + return int(value) + + if not isinstance(value, value_type): # type: ignore[arg-type] + msg = ( + f"Value {value} of type {type(value)} is invalid for {name}, " + f"expected value of type {value_type}" + ) + raise TypeError(msg) + return value diff --git a/music_assistant/helpers/app_vars.py b/music_assistant/helpers/app_vars.py new file mode 100644 index 00000000..2ec9bb8d --- /dev/null +++ b/music_assistant/helpers/app_vars.py @@ -0,0 +1,5 @@ +# pylint: skip-file +# fmt: off +# flake8: noqa +# ruff: noqa +(lambda __g: [(lambda __mod: [[[None for __g['app_var'], app_var.__name__ in [(lambda index: (lambda __l: [[AV(aap(__l['var'].encode()).decode()) for __l['var'] in [(vars.split('acb2')[__l['index']][::(-1)])]][0] for __l['index'] in [(index)]][0])({}), 'app_var')]][0] for __g['vars'] in [('3YTNyUDOyQTOacb2=EmN5M2YjdzMhljYzYzYhlDMmFGNlVTOmNDZwMzNxYzNacb2=UDMzEGOyADO1QWO5kDNygTMlJGN5QzNzIWOmZTOiVmMacb2yMTNzITNacb2=UDZhJmMldTZ3QTY4IjZ3kTNxYjN0czNwI2YxkTM5MjNacb2==QMh5WOmZnewM2d4UDblRzZacb20QzMwAjNacb2=QzNiRTO3EjMjFzMldjY3QTMwEDMwADMiNWZ5UWO3UWMacb2RJ1UJpXUPlzdvZUZ0w2VzVjMq1mblRnZvBHZ4x2RMZWbqhVS5JkdQ1WS38FVDpFTw9WcthGb41GaoV3dQV1QHNVRutUMjRFe09VeGh1RO1yQFtkZ3RnL5IERNJTVE5EerpWTzUkaPlWUYlFcKNET3FkaOlXSU9EMRpnT49maJdHaYpVa3lWS0MXVUpXSU5URWRlT1kUaPlWTzMGcKlXZuElZpFVMWtkSp9UaBhVZwo0QMl2Yq1UevtWVyEFbUNTWqlkNJNkWwRXbJNXSp5UMJpXVGpUaPl2YHJGaKlXZ')]][0] for __g['aap'] in [(__mod.b64decode)]][0])(__import__('base64', __g, __g, ('b64decode',), 0)) for __g['AV'] in [((lambda b, d: d.get('__metaclass__', getattr(b[0], '__class__', type(b[0])))('AV', b, d))((str,), (lambda __l: [__l for __l['__repr__'], __l['__repr__'].__name__ in [(lambda self: (lambda __l: [__name__ for __l['self'] in [(self)]][0])({}), '__repr__')]][0])({'__module__': __name__})))]][0])(globals()) # type: ignore diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py new file mode 100644 index 00000000..578bccf6 --- /dev/null +++ b/music_assistant/helpers/audio.py @@ -0,0 +1,983 @@ +"""Various helpers for audio streaming and manipulation.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import re +import struct +import time +from collections.abc import AsyncGenerator +from io import BytesIO +from typing import TYPE_CHECKING + +import aiofiles +from aiohttp import ClientTimeout +from music_assistant_models.enums import MediaType, StreamType, VolumeNormalizationMode +from music_assistant_models.errors import ( + InvalidDataError, + MediaNotFoundError, + MusicAssistantError, + ProviderUnavailableError, +) +from music_assistant_models.helpers.global_cache import set_global_cache_values +from music_assistant_models.media_items import AudioFormat, ContentType + +from music_assistant.constants import ( + CONF_EQ_BASS, + CONF_EQ_MID, + CONF_EQ_TREBLE, + CONF_OUTPUT_CHANNELS, + CONF_VOLUME_NORMALIZATION, + CONF_VOLUME_NORMALIZATION_RADIO, + CONF_VOLUME_NORMALIZATION_TARGET, + CONF_VOLUME_NORMALIZATION_TRACKS, + MASS_LOGGER_NAME, + VERBOSE_LOG_LEVEL, +) +from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads +from music_assistant.helpers.util import clean_stream_title + +from .ffmpeg import FFMpeg, get_ffmpeg_stream +from .playlists import IsHLSPlaylist, PlaylistItem, fetch_playlist, parse_m3u +from .process import AsyncProcess, check_output, communicate +from .throttle_retry import BYPASS_THROTTLER +from .util import TimedAsyncGenerator, create_tempfile, detect_charset + +if TYPE_CHECKING: + from music_assistant_models.config_entries import CoreConfig, PlayerConfig + from music_assistant_models.player_queue import QueueItem + from music_assistant_models.streamdetails import StreamDetails + + from music_assistant import MusicAssistant + +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.audio") + +# ruff: noqa: PLR0915 + +HTTP_HEADERS = {"User-Agent": "Lavf/60.16.100.MusicAssistant"} +HTTP_HEADERS_ICY = {**HTTP_HEADERS, "Icy-MetaData": "1"} + + +async def crossfade_pcm_parts( + fade_in_part: bytes, + fade_out_part: bytes, + pcm_format: AudioFormat, +) -> bytes: + """Crossfade two chunks of pcm/raw audio using ffmpeg.""" + sample_size = pcm_format.pcm_sample_size + # calculate the fade_length from the smallest chunk + fade_length = min(len(fade_in_part), len(fade_out_part)) / sample_size + fadeoutfile = create_tempfile() + async with aiofiles.open(fadeoutfile.name, "wb") as outfile: + await outfile.write(fade_out_part) + args = [ + # generic args + "ffmpeg", + "-hide_banner", + "-loglevel", + "quiet", + # fadeout part (as file) + "-acodec", + pcm_format.content_type.name.lower(), + "-f", + pcm_format.content_type.value, + "-ac", + str(pcm_format.channels), + "-ar", + str(pcm_format.sample_rate), + "-i", + fadeoutfile.name, + # fade_in part (stdin) + "-acodec", + pcm_format.content_type.name.lower(), + "-f", + pcm_format.content_type.value, + "-ac", + str(pcm_format.channels), + "-ar", + str(pcm_format.sample_rate), + "-i", + "-", + # filter args + "-filter_complex", + f"[0][1]acrossfade=d={fade_length}", + # output args + "-f", + pcm_format.content_type.value, + "-", + ] + _returncode, crossfaded_audio, _stderr = await communicate(args, fade_in_part) + if crossfaded_audio: + LOGGER.log( + VERBOSE_LOG_LEVEL, + "crossfaded 2 pcm chunks. fade_in_part: %s - " + "fade_out_part: %s - fade_length: %s seconds", + len(fade_in_part), + len(fade_out_part), + fade_length, + ) + return crossfaded_audio + # no crossfade_data, return original data instead + LOGGER.debug( + "crossfade of pcm chunks failed: not enough data? " "fade_in_part: %s - fade_out_part: %s", + len(fade_in_part), + len(fade_out_part), + ) + return fade_out_part + fade_in_part + + +async def strip_silence( + mass: MusicAssistant, # noqa: ARG001 + audio_data: bytes, + pcm_format: AudioFormat, + reverse: bool = False, +) -> bytes: + """Strip silence from begin or end of pcm audio using ffmpeg.""" + args = ["ffmpeg", "-hide_banner", "-loglevel", "quiet"] + args += [ + "-acodec", + pcm_format.content_type.name.lower(), + "-f", + pcm_format.content_type.value, + "-ac", + str(pcm_format.channels), + "-ar", + str(pcm_format.sample_rate), + "-i", + "-", + ] + # filter args + if reverse: + args += [ + "-af", + "areverse,atrim=start=0.2,silenceremove=start_periods=1:start_silence=0.1:start_threshold=0.02,areverse", + ] + else: + args += [ + "-af", + "atrim=start=0.2,silenceremove=start_periods=1:start_silence=0.1:start_threshold=0.02", + ] + # output args + args += ["-f", pcm_format.content_type.value, "-"] + _returncode, stripped_data, _stderr = await communicate(args, audio_data) + + # return stripped audio + bytes_stripped = len(audio_data) - len(stripped_data) + if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL): + seconds_stripped = round(bytes_stripped / pcm_format.pcm_sample_size, 2) + location = "end" if reverse else "begin" + LOGGER.log( + VERBOSE_LOG_LEVEL, + "stripped %s seconds of silence from %s of pcm audio. bytes stripped: %s", + seconds_stripped, + location, + bytes_stripped, + ) + return stripped_data + + +async def get_stream_details( + mass: MusicAssistant, + queue_item: QueueItem, + seek_position: int = 0, + fade_in: bool = False, + prefer_album_loudness: bool = False, +) -> StreamDetails: + """Get streamdetails for the given QueueItem. + + This is called just-in-time when a PlayerQueue wants a MediaItem to be played. + Do not try to request streamdetails in advance as this is expiring data. + param media_item: The QueueItem for which to request the streamdetails for. + """ + time_start = time.time() + LOGGER.debug("Getting streamdetails for %s", queue_item.uri) + if seek_position and (queue_item.media_type == MediaType.RADIO or not queue_item.duration): + LOGGER.warning("seeking is not possible on duration-less streams!") + seek_position = 0 + # we use a contextvar to bypass the throttler for this asyncio task/context + # this makes sure that playback has priority over other requests that may be + # happening in the background + BYPASS_THROTTLER.set(True) + if not queue_item.media_item: + # this should not happen, but guard it just in case + assert queue_item.streamdetails, "streamdetails required for non-mediaitem queueitems" + return queue_item.streamdetails + # always request the full library item as there might be other qualities available + media_item = ( + await mass.music.get_library_item_by_prov_id( + queue_item.media_item.media_type, + queue_item.media_item.item_id, + queue_item.media_item.provider, + ) + or queue_item.media_item + ) + # sort by quality and check track availability + for prov_media in sorted( + media_item.provider_mappings, key=lambda x: x.quality or 0, reverse=True + ): + if not prov_media.available: + LOGGER.debug(f"Skipping unavailable {prov_media}") + continue + # guard that provider is available + music_prov = mass.get_provider(prov_media.provider_instance) + if not music_prov: + LOGGER.debug(f"Skipping {prov_media} - provider not available") + continue # provider not available ? + # get streamdetails from provider + try: + streamdetails: StreamDetails = await music_prov.get_stream_details(prov_media.item_id) + except MusicAssistantError as err: + LOGGER.warning(str(err)) + else: + break + else: + raise MediaNotFoundError( + f"Unable to retrieve streamdetails for {queue_item.name} ({queue_item.uri})" + ) + + # work out how to handle radio stream + if ( + streamdetails.stream_type in (StreamType.ICY, StreamType.HLS, StreamType.HTTP) + and streamdetails.media_type == MediaType.RADIO + ): + resolved_url, stream_type = await resolve_radio_stream(mass, streamdetails.path) + streamdetails.path = resolved_url + streamdetails.stream_type = stream_type + # set queue_id on the streamdetails so we know what is being streamed + streamdetails.queue_id = queue_item.queue_id + # handle skip/fade_in details + streamdetails.seek_position = seek_position + streamdetails.fade_in = fade_in + if not streamdetails.duration: + streamdetails.duration = queue_item.duration + + # handle volume normalization details + if result := await mass.music.get_loudness( + streamdetails.item_id, + streamdetails.provider, + media_type=queue_item.media_type, + ): + streamdetails.loudness, streamdetails.loudness_album = result + streamdetails.prefer_album_loudness = prefer_album_loudness + player_settings = await mass.config.get_player_config(streamdetails.queue_id) + core_config = await mass.config.get_core_config("streams") + streamdetails.volume_normalization_mode = _get_normalization_mode( + core_config, player_settings, streamdetails + ) + streamdetails.target_loudness = player_settings.get_value(CONF_VOLUME_NORMALIZATION_TARGET) + + process_time = int((time.time() - time_start) * 1000) + LOGGER.debug("retrieved streamdetails for %s in %s milliseconds", queue_item.uri, process_time) + return streamdetails + + +async def get_media_stream( + mass: MusicAssistant, + streamdetails: StreamDetails, + pcm_format: AudioFormat, + audio_source: AsyncGenerator[bytes, None] | str, + filter_params: list[str] | None = None, + extra_input_args: list[str] | None = None, +) -> AsyncGenerator[bytes, None]: + """Get PCM audio stream for given media details.""" + logger = LOGGER.getChild("media_stream") + logger.debug("start media stream for: %s", streamdetails.uri) + strip_silence_begin = streamdetails.strip_silence_begin + strip_silence_end = streamdetails.strip_silence_end + if streamdetails.fade_in: + filter_params.append("afade=type=in:start_time=0:duration=3") + strip_silence_begin = False + bytes_sent = 0 + chunk_number = 0 + buffer: bytes = b"" + finished = False + + ffmpeg_proc = FFMpeg( + audio_input=audio_source, + input_format=streamdetails.audio_format, + output_format=pcm_format, + filter_params=filter_params, + extra_input_args=extra_input_args, + collect_log_history=True, + ) + try: + await ffmpeg_proc.start() + async for chunk in TimedAsyncGenerator( + ffmpeg_proc.iter_chunked(pcm_format.pcm_sample_size), 300 + ): + # for radio streams we just yield all chunks directly + if streamdetails.media_type == MediaType.RADIO: + yield chunk + bytes_sent += len(chunk) + continue + + chunk_number += 1 + # determine buffer size dynamically + if chunk_number < 5 and strip_silence_begin: + req_buffer_size = int(pcm_format.pcm_sample_size * 4) + elif chunk_number > 30 and strip_silence_end: + req_buffer_size = int(pcm_format.pcm_sample_size * 8) + else: + req_buffer_size = int(pcm_format.pcm_sample_size * 2) + + # always append to buffer + buffer += chunk + del chunk + + if len(buffer) < req_buffer_size: + # buffer is not full enough, move on + continue + + if chunk_number == 5 and strip_silence_begin: + # strip silence from begin of audio + chunk = await strip_silence( # noqa: PLW2901 + mass, buffer, pcm_format=pcm_format + ) + bytes_sent += len(chunk) + yield chunk + buffer = b"" + continue + + #### OTHER: enough data in buffer, feed to output + while len(buffer) > req_buffer_size: + yield buffer[: pcm_format.pcm_sample_size] + bytes_sent += pcm_format.pcm_sample_size + buffer = buffer[pcm_format.pcm_sample_size :] + + # end of audio/track reached + if strip_silence_end and buffer: + # strip silence from end of audio + buffer = await strip_silence( + mass, + buffer, + pcm_format=pcm_format, + reverse=True, + ) + # send remaining bytes in buffer + bytes_sent += len(buffer) + yield buffer + del buffer + finished = True + + finally: + await ffmpeg_proc.close() + + if bytes_sent == 0: + # edge case: no audio data was sent + streamdetails.stream_error = True + seconds_streamed = 0 + logger.warning("Stream error on %s", streamdetails.uri) + else: + # try to determine how many seconds we've streamed + seconds_streamed = bytes_sent / pcm_format.pcm_sample_size if bytes_sent else 0 + logger.debug( + "stream %s (with code %s) for %s - seconds streamed: %s", + "finished" if finished else "aborted", + ffmpeg_proc.returncode, + streamdetails.uri, + seconds_streamed, + ) + + streamdetails.seconds_streamed = seconds_streamed + # store accurate duration + if finished and not streamdetails.seek_position and seconds_streamed: + streamdetails.duration = seconds_streamed + + # parse loudnorm data if we have that collected + if ( + streamdetails.loudness is None + and streamdetails.volume_normalization_mode != VolumeNormalizationMode.DISABLED + and (finished or (seconds_streamed >= 300)) + ): + # if dynamic volume normalization is enabled and the entire track is streamed + # the loudnorm filter will output the measuremeet in the log, + # so we can use those directly instead of analyzing the audio + if loudness_details := parse_loudnorm(" ".join(ffmpeg_proc.log_history)): + logger.debug( + "Loudness measurement for %s: %s dB", + streamdetails.uri, + loudness_details, + ) + streamdetails.loudness = loudness_details + mass.create_task( + mass.music.set_loudness( + streamdetails.item_id, + streamdetails.provider, + loudness_details, + media_type=streamdetails.media_type, + ) + ) + else: + # no data from loudnorm filter found, we need to analyze the audio + # add background task to start analyzing the audio + task_id = f"analyze_loudness_{streamdetails.uri}" + mass.create_task(analyze_loudness, mass, streamdetails, task_id=task_id) + + # mark item as played in db if finished or streamed for 30 seconds + # NOTE that this is not the actual played time but the buffered time + # the queue controller will update the actual played time when the item is played + if finished or seconds_streamed > 30: + mass.create_task( + mass.music.mark_item_played( + streamdetails.media_type, + streamdetails.item_id, + streamdetails.provider, + ) + ) + + +def create_wave_header(samplerate=44100, channels=2, bitspersample=16, duration=None): + """Generate a wave header from given params.""" + file = BytesIO() + + # Generate format chunk + format_chunk_spec = b"<4sLHHLLHH" + format_chunk = struct.pack( + format_chunk_spec, + b"fmt ", # Chunk id + 16, # Size of this chunk (excluding chunk id and this field) + 1, # Audio format, 1 for PCM + channels, # Number of channels + int(samplerate), # Samplerate, 44100, 48000, etc. + int(samplerate * channels * (bitspersample / 8)), # Byterate + int(channels * (bitspersample / 8)), # Blockalign + bitspersample, # 16 bits for two byte samples, etc. + ) + # Generate data chunk + # duration = 3600*6.7 + data_chunk_spec = b"<4sL" + if duration is None: + # use max value possible + datasize = 4254768000 # = 6,7 hours at 44100/16 + else: + # calculate from duration + numsamples = samplerate * duration + datasize = int(numsamples * channels * (bitspersample / 8)) + data_chunk = struct.pack( + data_chunk_spec, + b"data", # Chunk id + int(datasize), # Chunk size (excluding chunk id and this field) + ) + sum_items = [ + # "WAVE" string following size field + 4, + # "fmt " + chunk size field + chunk size + struct.calcsize(format_chunk_spec), + # Size of data chunk spec + data size + struct.calcsize(data_chunk_spec) + datasize, + ] + # Generate main header + all_chunks_size = int(sum(sum_items)) + main_header_spec = b"<4sL4s" + main_header = struct.pack(main_header_spec, b"RIFF", all_chunks_size, b"WAVE") + # Write all the contents in + file.write(main_header) + file.write(format_chunk) + file.write(data_chunk) + + # return file.getvalue(), all_chunks_size + 8 + return file.getvalue() + + +async def resolve_radio_stream(mass: MusicAssistant, url: str) -> tuple[str, StreamType]: + """ + Resolve a streaming radio URL. + + Unwraps any playlists if needed. + Determines if the stream supports ICY metadata. + + Returns tuple; + - unfolded URL as string + - StreamType to determine ICY (radio) or HLS stream. + """ + cache_base_key = "resolved_radio_info" + if cache := await mass.cache.get(url, base_key=cache_base_key): + return cache + stream_type = StreamType.HTTP + resolved_url = url + timeout = ClientTimeout(total=0, connect=10, sock_read=5) + try: + async with mass.http_session.get( + url, headers=HTTP_HEADERS_ICY, allow_redirects=True, timeout=timeout + ) as resp: + headers = resp.headers + resp.raise_for_status() + if not resp.headers: + raise InvalidDataError("no headers found") + if headers.get("icy-metaint") is not None: + stream_type = StreamType.ICY + if ( + url.endswith((".m3u", ".m3u8", ".pls")) + or ".m3u?" in url + or ".m3u8?" in url + or ".pls?" in url + or "audio/x-mpegurl" in headers.get("content-type") + or "audio/x-scpls" in headers.get("content-type", "") + ): + # url is playlist, we need to unfold it + try: + substreams = await fetch_playlist(mass, url) + if not any(x for x in substreams if x.length): + for line in substreams: + if not line.is_url: + continue + # unfold first url of playlist + return await resolve_radio_stream(mass, line.path) + raise InvalidDataError("No content found in playlist") + except IsHLSPlaylist: + stream_type = StreamType.HLS + + except Exception as err: + LOGGER.warning("Error while parsing radio URL %s: %s", url, err) + return (url, stream_type) + + result = (resolved_url, stream_type) + cache_expiration = 3600 * 3 + await mass.cache.set(url, result, expiration=cache_expiration, base_key=cache_base_key) + return result + + +async def get_icy_radio_stream( + mass: MusicAssistant, url: str, streamdetails: StreamDetails +) -> AsyncGenerator[bytes, None]: + """Get (radio) audio stream from HTTP, including ICY metadata retrieval.""" + timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) + LOGGER.debug("Start streaming radio with ICY metadata from url %s", url) + async with mass.http_session.get( + url, allow_redirects=True, headers=HTTP_HEADERS_ICY, timeout=timeout + ) as resp: + headers = resp.headers + meta_int = int(headers["icy-metaint"]) + while True: + try: + yield await resp.content.readexactly(meta_int) + meta_byte = await resp.content.readexactly(1) + if meta_byte == b"\x00": + continue + meta_length = ord(meta_byte) * 16 + meta_data = await resp.content.readexactly(meta_length) + except asyncio.exceptions.IncompleteReadError: + break + if not meta_data: + continue + meta_data = meta_data.rstrip(b"\0") + stream_title = re.search(rb"StreamTitle='([^']*)';", meta_data) + if not stream_title: + continue + try: + # in 99% of the cases the stream title is utf-8 encoded + stream_title = stream_title.group(1).decode("utf-8") + except UnicodeDecodeError: + # fallback to iso-8859-1 + stream_title = stream_title.group(1).decode("iso-8859-1", errors="replace") + cleaned_stream_title = clean_stream_title(stream_title) + if cleaned_stream_title != streamdetails.stream_title: + LOGGER.log(VERBOSE_LOG_LEVEL, "ICY Radio streamtitle original: %s", stream_title) + LOGGER.log( + VERBOSE_LOG_LEVEL, "ICY Radio streamtitle cleaned: %s", cleaned_stream_title + ) + streamdetails.stream_title = cleaned_stream_title + + +async def get_hls_substream( + mass: MusicAssistant, + url: str, +) -> PlaylistItem: + """Select the (highest quality) HLS substream for given HLS playlist/URL.""" + timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) + # fetch master playlist and select (best) child playlist + # https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-10 + async with mass.http_session.get( + url, allow_redirects=True, headers=HTTP_HEADERS, timeout=timeout + ) as resp: + resp.raise_for_status() + raw_data = await resp.read() + encoding = resp.charset or await detect_charset(raw_data) + master_m3u_data = raw_data.decode(encoding) + substreams = parse_m3u(master_m3u_data) + # There is a chance that we did not get a master playlist with subplaylists + # but just a single master/sub playlist with the actual audio stream(s) + # so we need to detect if the playlist child's contain audio streams or + # sub-playlists. + if any( + x + for x in substreams + if (x.length or x.path.endswith((".mp4", ".aac"))) + and not x.path.endswith((".m3u", ".m3u8")) + ): + return PlaylistItem(path=url, key=substreams[0].key) + # sort substreams on best quality (highest bandwidth) when available + if any(x for x in substreams if x.stream_info): + substreams.sort(key=lambda x: int(x.stream_info.get("BANDWIDTH", "0")), reverse=True) + substream = substreams[0] + if not substream.path.startswith("http"): + # path is relative, stitch it together + base_path = url.rsplit("/", 1)[0] + substream.path = base_path + "/" + substream.path + return substream + + +async def get_http_stream( + mass: MusicAssistant, + url: str, + streamdetails: StreamDetails, + seek_position: int = 0, +) -> AsyncGenerator[bytes, None]: + """Get audio stream from HTTP.""" + LOGGER.debug("Start HTTP stream for %s (seek_position %s)", streamdetails.uri, seek_position) + if seek_position: + assert streamdetails.duration, "Duration required for seek requests" + # try to get filesize with a head request + seek_supported = streamdetails.can_seek + if seek_position or not streamdetails.size: + async with mass.http_session.head(url, allow_redirects=True, headers=HTTP_HEADERS) as resp: + resp.raise_for_status() + if size := resp.headers.get("Content-Length"): + streamdetails.size = int(size) + seek_supported = resp.headers.get("Accept-Ranges") == "bytes" + # headers + headers = {**HTTP_HEADERS} + timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) + skip_bytes = 0 + if seek_position and streamdetails.size: + skip_bytes = int(streamdetails.size / streamdetails.duration * seek_position) + headers["Range"] = f"bytes={skip_bytes}-{streamdetails.size}" + + # seeking an unknown or container format is not supported due to the (moov) headers + if seek_position and ( + not seek_supported + or streamdetails.audio_format.content_type + in ( + ContentType.UNKNOWN, + ContentType.M4A, + ContentType.M4B, + ) + ): + LOGGER.warning( + "Seeking in %s (%s) not possible.", + streamdetails.uri, + streamdetails.audio_format.output_format_str, + ) + seek_position = 0 + streamdetails.seek_position = 0 + + # start the streaming from http + bytes_received = 0 + async with mass.http_session.get( + url, allow_redirects=True, headers=headers, timeout=timeout + ) as resp: + is_partial = resp.status == 206 + if seek_position and not is_partial: + raise InvalidDataError("HTTP source does not support seeking!") + resp.raise_for_status() + async for chunk in resp.content.iter_any(): + bytes_received += len(chunk) + yield chunk + + # store size on streamdetails for later use + if not streamdetails.size: + streamdetails.size = bytes_received + LOGGER.debug( + "Finished HTTP stream for %s (transferred %s/%s bytes)", + streamdetails.uri, + bytes_received, + streamdetails.size, + ) + + +async def get_file_stream( + mass: MusicAssistant, # noqa: ARG001 + filename: str, + streamdetails: StreamDetails, + seek_position: int = 0, +) -> AsyncGenerator[bytes, None]: + """Get audio stream from local accessible file.""" + if seek_position: + assert streamdetails.duration, "Duration required for seek requests" + if not streamdetails.size: + stat = await asyncio.to_thread(os.stat, filename) + streamdetails.size = stat.st_size + + # seeking an unknown or container format is not supported due to the (moov) headers + if seek_position and ( + streamdetails.audio_format.content_type + in ( + ContentType.UNKNOWN, + ContentType.M4A, + ContentType.M4B, + ContentType.MP4, + ) + ): + LOGGER.warning( + "Seeking in %s (%s) not possible.", + streamdetails.uri, + streamdetails.audio_format.output_format_str, + ) + seek_position = 0 + streamdetails.seek_position = 0 + + chunk_size = get_chunksize(streamdetails.audio_format) + async with aiofiles.open(streamdetails.data, "rb") as _file: + if seek_position: + seek_pos = int((streamdetails.size / streamdetails.duration) * seek_position) + await _file.seek(seek_pos) + # yield chunks of data from file + while True: + data = await _file.read(chunk_size) + if not data: + break + yield data + + +async def check_audio_support() -> tuple[bool, bool, str]: + """Check if ffmpeg is present (with/without libsoxr support).""" + # check for FFmpeg presence + returncode, output = await check_output("ffmpeg", "-version") + ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode() + + # use globals as in-memory cache + version = output.decode().split("ffmpeg version ")[1].split(" ")[0].split("-")[0] + libsoxr_support = "enable-libsoxr" in output.decode() + result = (ffmpeg_present, libsoxr_support, version) + # store in global cache for easy access by 'get_ffmpeg_args' + await set_global_cache_values({"ffmpeg_support": result}) + return result + + +async def get_preview_stream( + mass: MusicAssistant, + provider_instance_id_or_domain: str, + track_id: str, +) -> AsyncGenerator[bytes, None]: + """Create a 30 seconds preview audioclip for the given streamdetails.""" + if not (music_prov := mass.get_provider(provider_instance_id_or_domain)): + raise ProviderUnavailableError + streamdetails = await music_prov.get_stream_details(track_id) + async for chunk in get_ffmpeg_stream( + audio_input=music_prov.get_audio_stream(streamdetails, 30) + if streamdetails.stream_type == StreamType.CUSTOM + else streamdetails.path, + input_format=streamdetails.audio_format, + output_format=AudioFormat(content_type=ContentType.AAC), + extra_input_args=["-to", "30"], + ): + yield chunk + + +async def get_silence( + duration: int, + output_format: AudioFormat, +) -> AsyncGenerator[bytes, None]: + """Create stream of silence, encoded to format of choice.""" + if output_format.content_type.is_pcm(): + # pcm = just zeros + for _ in range(duration): + yield b"\0" * int(output_format.sample_rate * (output_format.bit_depth / 8) * 2) + return + if output_format.content_type == ContentType.WAV: + # wav silence = wave header + zero's + yield create_wave_header( + samplerate=output_format.sample_rate, + channels=2, + bitspersample=output_format.bit_depth, + duration=duration, + ) + for _ in range(duration): + yield b"\0" * int(output_format.sample_rate * (output_format.bit_depth / 8) * 2) + return + # use ffmpeg for all other encodings + args = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "quiet", + "-f", + "lavfi", + "-i", + f"anullsrc=r={output_format.sample_rate}:cl={'stereo'}", + "-t", + str(duration), + "-f", + output_format.output_format_str, + "-", + ] + async with AsyncProcess(args, stdout=True) as ffmpeg_proc: + async for chunk in ffmpeg_proc.iter_chunked(): + yield chunk + + +def get_chunksize( + fmt: AudioFormat, + seconds: int = 1, +) -> int: + """Get a default chunk/file size for given contenttype in bytes.""" + pcm_size = int(fmt.sample_rate * (fmt.bit_depth / 8) * fmt.channels * seconds) + if fmt.content_type.is_pcm() or fmt.content_type == ContentType.WAV: + return pcm_size + if fmt.content_type in (ContentType.WAV, ContentType.AIFF, ContentType.DSF): + return pcm_size + if fmt.bit_rate: + return int(((fmt.bit_rate * 1000) / 8) * seconds) + if fmt.content_type in (ContentType.FLAC, ContentType.WAVPACK, ContentType.ALAC): + # assume 74.7% compression ratio (level 0) + # source: https://z-issue.com/wp/flac-compression-level-comparison/ + return int(pcm_size * 0.747) + if fmt.content_type in (ContentType.MP3, ContentType.OGG): + return int((320000 / 8) * seconds) + if fmt.content_type in (ContentType.AAC, ContentType.M4A): + return int((256000 / 8) * seconds) + return int((320000 / 8) * seconds) + + +def get_player_filter_params( + mass: MusicAssistant, + player_id: str, +) -> list[str]: + """Get player specific filter parameters for ffmpeg (if any).""" + # collect all players-specific filter args + # TODO: add convolution/DSP/roomcorrections here?! + filter_params = [] + + # the below is a very basic 3-band equalizer, + # this could be a lot more sophisticated at some point + if (eq_bass := mass.config.get_raw_player_config_value(player_id, CONF_EQ_BASS, 0)) != 0: + filter_params.append(f"equalizer=frequency=100:width=200:width_type=h:gain={eq_bass}") + if (eq_mid := mass.config.get_raw_player_config_value(player_id, CONF_EQ_MID, 0)) != 0: + filter_params.append(f"equalizer=frequency=900:width=1800:width_type=h:gain={eq_mid}") + if (eq_treble := mass.config.get_raw_player_config_value(player_id, CONF_EQ_TREBLE, 0)) != 0: + filter_params.append(f"equalizer=frequency=9000:width=18000:width_type=h:gain={eq_treble}") + # handle output mixing only left or right + conf_channels = mass.config.get_raw_player_config_value( + player_id, CONF_OUTPUT_CHANNELS, "stereo" + ) + if conf_channels == "left": + filter_params.append("pan=mono|c0=FL") + elif conf_channels == "right": + filter_params.append("pan=mono|c0=FR") + + # add a peak limiter at the end of the filter chain + filter_params.append("alimiter=limit=-2dB:level=false:asc=true") + + return filter_params + + +def parse_loudnorm(raw_stderr: bytes | str) -> float | None: + """Parse Loudness measurement from ffmpeg stderr output.""" + stderr_data = raw_stderr.decode() if isinstance(raw_stderr, bytes) else raw_stderr + if "[Parsed_loudnorm_" not in stderr_data: + return None + stderr_data = stderr_data.split("[Parsed_loudnorm_")[1] + stderr_data = "{" + stderr_data.rsplit("{")[-1].strip() + stderr_data = stderr_data.rsplit("}")[0].strip() + "}" + try: + loudness_data = json_loads(stderr_data) + except JSON_DECODE_EXCEPTIONS: + return None + return float(loudness_data["input_i"]) + + +async def analyze_loudness( + mass: MusicAssistant, + streamdetails: StreamDetails, +) -> None: + """Analyze media item's audio, to calculate EBU R128 loudness.""" + if result := await mass.music.get_loudness( + streamdetails.item_id, + streamdetails.provider, + media_type=streamdetails.media_type, + ): + # only when needed we do the analyze job + streamdetails.loudness = result + return + + logger = LOGGER.getChild("analyze_loudness") + logger.debug("Start analyzing audio for %s", streamdetails.uri) + + extra_input_args = [ + # limit to 10 minutes to reading too much in memory + "-t", + "600", + ] + if streamdetails.stream_type == StreamType.CUSTOM: + audio_source = mass.get_provider(streamdetails.provider).get_audio_stream( + streamdetails, + ) + elif streamdetails.stream_type == StreamType.HLS: + substream = await get_hls_substream(mass, streamdetails.path) + audio_source = substream.path + elif streamdetails.stream_type == StreamType.ENCRYPTED_HTTP: + audio_source = streamdetails.path + extra_input_args += ["-decryption_key", streamdetails.decryption_key] + else: + audio_source = streamdetails.path + + # calculate BS.1770 R128 integrated loudness with ffmpeg + async with FFMpeg( + audio_input=audio_source, + input_format=streamdetails.audio_format, + output_format=streamdetails.audio_format, + audio_output="NULL", + filter_params=["ebur128=framelog=verbose"], + extra_input_args=extra_input_args, + collect_log_history=True, + ) as ffmpeg_proc: + await ffmpeg_proc.wait() + log_lines = ffmpeg_proc.log_history + log_lines_str = "\n".join(log_lines) + try: + loudness_str = ( + log_lines_str.split("Integrated loudness")[1].split("I:")[1].split("LUFS")[0] + ) + loudness = float(loudness_str.strip()) + except (IndexError, ValueError, AttributeError): + LOGGER.warning( + "Could not determine integrated loudness of %s - %s", + streamdetails.uri, + log_lines_str or "received empty value", + ) + else: + streamdetails.loudness = loudness + await mass.music.set_loudness( + streamdetails.item_id, + streamdetails.provider, + loudness, + media_type=streamdetails.media_type, + ) + logger.debug( + "Integrated loudness of %s is: %s", + streamdetails.uri, + loudness, + ) + + +def _get_normalization_mode( + core_config: CoreConfig, player_config: PlayerConfig, streamdetails: StreamDetails +) -> VolumeNormalizationMode: + if not player_config.get_value(CONF_VOLUME_NORMALIZATION): + # disabled for this player + return VolumeNormalizationMode.DISABLED + # work out preference for track or radio + preference = VolumeNormalizationMode( + core_config.get_value( + CONF_VOLUME_NORMALIZATION_RADIO + if streamdetails.media_type == MediaType.RADIO + else CONF_VOLUME_NORMALIZATION_TRACKS, + ) + ) + + # handle no measurement available but fallback to dynamic mode is allowed + if streamdetails.loudness is None and preference == VolumeNormalizationMode.FALLBACK_DYNAMIC: + return VolumeNormalizationMode.DYNAMIC + + # handle no measurement available and no fallback allowed + if streamdetails.loudness is None and preference == VolumeNormalizationMode.MEASUREMENT_ONLY: + return VolumeNormalizationMode.DISABLED + + # handle no measurement available and fallback to fixed gain is allowed + if streamdetails.loudness is None and preference == VolumeNormalizationMode.FALLBACK_FIXED_GAIN: + return VolumeNormalizationMode.FIXED_GAIN + + # simply return the preference + return preference diff --git a/music_assistant/helpers/auth.py b/music_assistant/helpers/auth.py new file mode 100644 index 00000000..04ae1947 --- /dev/null +++ b/music_assistant/helpers/auth.py @@ -0,0 +1,82 @@ +"""Helper(s) to deal with authentication for (music) providers.""" + +from __future__ import annotations + +import asyncio +from types import TracebackType +from typing import TYPE_CHECKING + +from aiohttp.web import Request, Response +from music_assistant_models.enums import EventType +from music_assistant_models.errors import LoginFailed + +if TYPE_CHECKING: + from music_assistant import MusicAssistant + + +class AuthenticationHelper: + """Context manager helper class for authentication with a forward and redirect URL.""" + + def __init__(self, mass: MusicAssistant, session_id: str) -> None: + """ + Initialize the Authentication Helper. + + Params: + - url: The URL the user needs to open for authentication. + - session_id: a unique id for this auth session. + """ + self.mass = mass + self.session_id = session_id + self._callback_response: asyncio.Queue[dict[str, str]] = asyncio.Queue(1) + + @property + def callback_url(self) -> str: + """Return the callback URL.""" + return f"{self.mass.streams.base_url}/callback/{self.session_id}" + + async def __aenter__(self) -> AuthenticationHelper: + """Enter context manager.""" + self.mass.streams.register_dynamic_route( + f"/callback/{self.session_id}", self._handle_callback, "GET" + ) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit context manager.""" + self.mass.streams.unregister_dynamic_route(f"/callback/{self.session_id}", "GET") + + async def authenticate(self, auth_url: str, timeout: int = 60) -> dict[str, str]: + """Start the auth process and return any query params if received on the callback.""" + self.send_url(auth_url) + return await self.wait_for_callback(timeout) + + def send_url(self, auth_url: str) -> None: + """Send the user to the given URL to authenticate (or fill in a code).""" + # redirect the user in the frontend to the auth url + self.mass.signal_event(EventType.AUTH_SESSION, self.session_id, auth_url) + + async def wait_for_callback(self, timeout: int = 60) -> dict[str, str]: + """Wait for the external party to call the callback and return any query strings.""" + try: + async with asyncio.timeout(timeout): + return await self._callback_response.get() + except TimeoutError as err: + raise LoginFailed("Timeout while waiting for authentication callback") from err + + async def _handle_callback(self, request: Request) -> Response: + """Handle callback response.""" + params = dict(request.query) + await self._callback_response.put(params) + return_html = """ + <html> + <body onload="window.close();"> + Authentication completed, you may now close this window. + </body> + </html> + """ + return Response(body=return_html, headers={"content-type": "text/html"}) diff --git a/music_assistant/helpers/compare.py b/music_assistant/helpers/compare.py new file mode 100644 index 00000000..1d10fecb --- /dev/null +++ b/music_assistant/helpers/compare.py @@ -0,0 +1,474 @@ +"""Several helper/utils to compare objects.""" + +from __future__ import annotations + +import re +from difflib import SequenceMatcher + +import unidecode +from music_assistant_models.enums import ExternalID, MediaType +from music_assistant_models.media_items import ( + Album, + Artist, + ItemMapping, + MediaItem, + MediaItemMetadata, + MediaItemType, + Playlist, + Radio, + Track, +) + +IGNORE_VERSIONS = ( + "explicit", # explicit is matched separately + "music from and inspired by the motion picture", + "original soundtrack", + "hi-res", # quality is handled separately +) + + +def compare_media_item( + base_item: MediaItemType | ItemMapping, + compare_item: MediaItemType | ItemMapping, + strict: bool = True, +) -> bool | None: + """Compare two media items and return True if they match.""" + if base_item.media_type == MediaType.ARTIST and compare_item.media_type == MediaType.ARTIST: + return compare_artist(base_item, compare_item, strict) + if base_item.media_type == MediaType.ALBUM and compare_item.media_type == MediaType.ALBUM: + return compare_album(base_item, compare_item, strict) + if base_item.media_type == MediaType.TRACK and compare_item.media_type == MediaType.TRACK: + return compare_track(base_item, compare_item, strict) + if base_item.media_type == MediaType.PLAYLIST and compare_item.media_type == MediaType.PLAYLIST: + return compare_playlist(base_item, compare_item, strict) + if base_item.media_type == MediaType.RADIO and compare_item.media_type == MediaType.RADIO: + return compare_radio(base_item, compare_item, strict) + return compare_item_mapping(base_item, compare_item, strict) + + +def compare_artist( + base_item: Artist | ItemMapping, + compare_item: Artist | ItemMapping, + strict: bool = True, +) -> bool | None: + """Compare two artist items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # return early on exact item_id match + if compare_item_ids(base_item, compare_item): + return True + # return early on (un)matched external id + for ext_id in (ExternalID.DISCOGS, ExternalID.MB_ARTIST, ExternalID.TADB): + external_id_match = compare_external_ids( + base_item.external_ids, compare_item.external_ids, ext_id + ) + if external_id_match is not None: + return external_id_match + # finally comparing on (exact) name match + return compare_strings(base_item.name, compare_item.name, strict=strict) + + +def compare_album( + base_item: Album | ItemMapping | None, + compare_item: Album | ItemMapping | None, + strict: bool = True, +) -> bool | None: + """Compare two album items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # return early on exact item_id match + if compare_item_ids(base_item, compare_item): + return True + + # return early on (un)matched external id + for ext_id in ( + ExternalID.DISCOGS, + ExternalID.MB_ALBUM, + ExternalID.TADB, + ExternalID.ASIN, + ExternalID.BARCODE, + ): + external_id_match = compare_external_ids( + base_item.external_ids, compare_item.external_ids, ext_id + ) + if external_id_match is not None: + return external_id_match + + # compare version + if not compare_version(base_item.version, compare_item.version): + return False + # compare name + if not compare_strings(base_item.name, compare_item.name, strict=True): + return False + if not strict and (isinstance(base_item, ItemMapping) or isinstance(compare_item, ItemMapping)): + return True + # for strict matching we REQUIRE both items to be a real album object + assert isinstance(base_item, Album) + assert isinstance(compare_item, Album) + # compare year + if base_item.year and compare_item.year and base_item.year != compare_item.year: + return False + # compare explicitness + if compare_explicit(base_item.metadata, compare_item.metadata) is False: + return False + # compare album artist(s) + return compare_artists(base_item.artists, compare_item.artists, not strict) + + +def compare_track( + base_item: Track | None, + compare_item: Track | None, + strict: bool = True, + track_albums: list[Album] | None = None, +) -> bool: + """Compare two track items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # return early on exact item_id match + if compare_item_ids(base_item, compare_item): + return True + # return early on (un)matched primary/unique external id + for ext_id in ( + ExternalID.MB_RECORDING, + ExternalID.MB_TRACK, + ExternalID.ACOUSTID, + ): + external_id_match = compare_external_ids( + base_item.external_ids, compare_item.external_ids, ext_id + ) + if external_id_match is not None: + return external_id_match + # check secondary external id matches + for ext_id in ( + ExternalID.DISCOGS, + ExternalID.TADB, + ExternalID.ISRC, + ExternalID.ASIN, + ): + external_id_match = compare_external_ids( + base_item.external_ids, compare_item.external_ids, ext_id + ) + if external_id_match is True: + # we got a 'soft-match' on a secondary external id (like ISRC) + # but we do a double check on duration + if abs(base_item.duration - compare_item.duration) <= 2: + return True + + # compare name + if not compare_strings(base_item.name, compare_item.name, strict=True): + return False + # track artist(s) must match + if not compare_artists(base_item.artists, compare_item.artists, any_match=not strict): + return False + # track version must match + if strict and not compare_version(base_item.version, compare_item.version): + return False + # check if both tracks are (not) explicit + if base_item.metadata.explicit is None and isinstance(base_item.album, Album): + base_item.metadata.explicit = base_item.album.metadata.explicit + if compare_item.metadata.explicit is None and isinstance(compare_item.album, Album): + compare_item.metadata.explicit = compare_item.album.metadata.explicit + if strict and compare_explicit(base_item.metadata, compare_item.metadata) is False: + return False + + # exact albumtrack match = 100% match + if ( + base_item.album + and compare_item.album + and compare_album(base_item.album, compare_item.album, False) + and base_item.disc_number + and compare_item.disc_number + and base_item.track_number + and compare_item.track_number + and base_item.disc_number == compare_item.disc_number + and base_item.track_number == compare_item.track_number + ): + return True + + # fallback: exact album match and (near-exact) track duration match + if ( + base_item.album is not None + and compare_item.album is not None + and (base_item.track_number == 0 or compare_item.track_number == 0) + and compare_album(base_item.album, compare_item.album, False) + and abs(base_item.duration - compare_item.duration) <= 3 + ): + return True + + # fallback: additional compare albums provided for base track + if ( + compare_item.album is not None + and track_albums + and abs(base_item.duration - compare_item.duration) <= 3 + ): + for track_album in track_albums: + if compare_album(track_album, compare_item.album, False): + return True + + # fallback edge case: albumless track with same duration + if ( + base_item.album is None + and compare_item.album is None + and base_item.disc_number == 0 + and compare_item.disc_number == 0 + and base_item.track_number == 0 + and compare_item.track_number == 0 + and base_item.duration == compare_item.duration + ): + return True + + if strict: + # in strict mode, we require an exact album match so return False here + return False + + # Accept last resort (in non strict mode): (near) exact duration, + # otherwise fail all other cases. + # Note that as this stage, all other info already matches, + # such as title, artist etc. + return abs(base_item.duration - compare_item.duration) <= 2 + + +def compare_playlist( + base_item: Playlist | ItemMapping, + compare_item: Playlist | ItemMapping, + strict: bool = True, +) -> bool | None: + """Compare two Playlist items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # require (exact) name match + if not compare_strings(base_item.name, compare_item.name, strict=strict): + return False + # require exact owner match (if not ItemMapping) + if isinstance(base_item, Playlist) and isinstance(compare_item, Playlist): + if not compare_strings(base_item.owner, compare_item.owner): + return False + # a playlist is always unique - so do a strict compare on item id(s) + return compare_item_ids(base_item, compare_item) + + +def compare_radio( + base_item: Radio | ItemMapping, + compare_item: Radio | ItemMapping, + strict: bool = True, +) -> bool | None: + """Compare two Radio items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # return early on exact item_id match + if compare_item_ids(base_item, compare_item): + return True + # compare version + if not compare_version(base_item.version, compare_item.version): + return False + # finally comparing on (exact) name match + return compare_strings(base_item.name, compare_item.name, strict=strict) + + +def compare_item_mapping( + base_item: ItemMapping, + compare_item: ItemMapping, + strict: bool = True, +) -> bool | None: + """Compare two ItemMapping items and return True if they match.""" + if base_item is None or compare_item is None: + return False + # return early on exact item_id match + if compare_item_ids(base_item, compare_item): + return True + # return early on (un)matched external id + external_id_match = compare_external_ids(base_item.external_ids, compare_item.external_ids) + if external_id_match is not None: + return external_id_match + # compare version + if not compare_version(base_item.version, compare_item.version): + return False + # finally comparing on (exact) name match + return compare_strings(base_item.name, compare_item.name, strict=strict) + + +def compare_artists( + base_items: list[Artist | ItemMapping], + compare_items: list[Artist | ItemMapping], + any_match: bool = True, +) -> bool: + """Compare two lists of artist and return True if both lists match (exactly).""" + if not base_items or not compare_items: + return False + # match if first artist matches in both lists + if compare_artist(base_items[0], compare_items[0]): + return True + # compare the artist lists + matches = 0 + for base_item in base_items: + for compare_item in compare_items: + if compare_artist(base_item, compare_item): + if any_match: + return True + matches += 1 + return len(base_items) == len(compare_items) == matches + + +def compare_albums( + base_items: list[Album | ItemMapping], + compare_items: list[Album | ItemMapping], + any_match: bool = True, +) -> bool: + """Compare two lists of albums and return True if a match was found.""" + matches = 0 + for base_item in base_items: + for compare_item in compare_items: + if compare_album(base_item, compare_item): + if any_match: + return True + matches += 1 + return len(base_items) == matches + + +def compare_item_ids( + base_item: MediaItem | ItemMapping, compare_item: MediaItem | ItemMapping +) -> bool: + """Compare item_id(s) of two media items.""" + if not base_item.provider or not compare_item.provider: + return False + if not base_item.item_id or not compare_item.item_id: + return False + if base_item.provider == compare_item.provider and base_item.item_id == compare_item.item_id: + return True + + base_prov_ids = getattr(base_item, "provider_mappings", None) + compare_prov_ids = getattr(compare_item, "provider_mappings", None) + + if base_prov_ids is not None: + for prov_l in base_item.provider_mappings: + if ( + prov_l.provider_domain == compare_item.provider + and prov_l.item_id == compare_item.item_id + ): + return True + + if compare_prov_ids is not None: + for prov_r in compare_item.provider_mappings: + if prov_r.provider_domain == base_item.provider and prov_r.item_id == base_item.item_id: + return True + + if base_prov_ids is not None and compare_prov_ids is not None: + for prov_l in base_item.provider_mappings: + for prov_r in compare_item.provider_mappings: + if prov_l.provider_domain != prov_r.provider_domain: + continue + if prov_l.item_id == prov_r.item_id: + return True + return False + + +def compare_external_ids( + external_ids_base: set[tuple[ExternalID, str]], + external_ids_compare: set[tuple[ExternalID, str]], + external_id_type: ExternalID, +) -> bool | None: + """Compare external ids and return True if a match was found.""" + base_ids = {x[1] for x in external_ids_base if x[0] == external_id_type} + if not base_ids: + # return early if the requested external id type is not present in the base set + return None + compare_ids = {x[1] for x in external_ids_compare if x[0] == external_id_type} + if not compare_ids: + # return early if the requested external id type is not present in the compare set + return None + for base_id in base_ids: + if base_id in compare_ids: + return True + # handle upc stored as EAN-13 barcode + if external_id_type == ExternalID.BARCODE and len(base_id) == 12: + if f"0{base_id}" in compare_ids: + return True + # handle EAN-13 stored as UPC barcode + if external_id_type == ExternalID.BARCODE and len(base_id) == 13: + if base_id[1:] in compare_ids: + return True + # return false if the identifier is unique (e.g. musicbrainz id) + if external_id_type.is_unique: + return False + return None + + +def create_safe_string(input_str: str, lowercase: bool = True, replace_space: bool = False) -> str: + """Return clean lowered string for compare actions.""" + input_str = input_str.lower().strip() if lowercase else input_str.strip() + unaccented_string = unidecode.unidecode(input_str) + regex = r"[^a-zA-Z0-9]" if replace_space else r"[^a-zA-Z0-9 ]" + return re.sub(regex, "", unaccented_string) + + +def loose_compare_strings(base: str, alt: str) -> bool: + """Compare strings and return True even on partial match.""" + # this is used to display 'versions' of the same track/album + # where we account for other spelling or some additional wording in the title + word_count = len(base.split(" ")) + if word_count == 1 and len(base) < 10: + return compare_strings(base, alt, False) + base_comp = create_safe_string(base) + alt_comp = create_safe_string(alt) + if base_comp in alt_comp: + return True + return alt_comp in base_comp + + +def compare_strings(str1: str, str2: str, strict: bool = True) -> bool: + """Compare strings and return True if we have an (almost) perfect match.""" + if not str1 or not str2: + return False + str1_lower = str1.lower() + str2_lower = str2.lower() + if strict: + return str1_lower == str2_lower + # return early if total length mismatch + if abs(len(str1) - len(str2)) > 4: + return False + # handle '&' vs 'And' + if " & " in str1_lower and " and " in str2_lower: + str2 = str2_lower.replace(" and ", " & ") + elif " and " in str1_lower and " & " in str2: + str2 = str2.replace(" & ", " and ") + if create_safe_string(str1) == create_safe_string(str2): + return True + # last resort: use difflib to compare strings + required_accuracy = 0.9 if (len(str1) + len(str2)) > 18 else 0.8 + return SequenceMatcher(a=str1_lower, b=str2).ratio() > required_accuracy + + +def compare_version(base_version: str, compare_version: str) -> bool: + """Compare version string.""" + if not base_version and not compare_version: + return True + if not base_version and compare_version.lower() in IGNORE_VERSIONS: + return True + if not compare_version and base_version.lower() in IGNORE_VERSIONS: + return True + if not base_version and compare_version: + return False + if base_version and not compare_version: + return False + + if " " not in base_version and " " not in compare_version: + return compare_strings(base_version, compare_version, False) + + # do this the hard way as sometimes the version string is in the wrong order + base_versions = sorted(base_version.lower().split(" ")) + compare_versions = sorted(compare_version.lower().split(" ")) + # filter out words we can ignore (such as 'version') + ignore_words = [*IGNORE_VERSIONS, "version", "edition", "variant", "versie", "versione"] + base_versions = [x for x in base_versions if x not in ignore_words] + compare_versions = [x for x in compare_versions if x not in ignore_words] + + return base_versions == compare_versions + + +def compare_explicit(base: MediaItemMetadata, compare: MediaItemMetadata) -> bool | None: + """Compare if explicit is same in metadata.""" + if base.explicit is not None and compare.explicit is not None: + # explicitness info is not always present in metadata + # only strict compare them if both have the info set + return base.explicit == compare.explicit + return None diff --git a/music_assistant/helpers/database.py b/music_assistant/helpers/database.py new file mode 100644 index 00000000..9e0e37e1 --- /dev/null +++ b/music_assistant/helpers/database.py @@ -0,0 +1,254 @@ +"""Database helpers and logic.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import time +from contextlib import asynccontextmanager +from sqlite3 import OperationalError +from typing import TYPE_CHECKING, Any + +import aiosqlite + +from music_assistant.constants import MASS_LOGGER_NAME + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Mapping + +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.database") + +ENABLE_DEBUG = os.environ.get("PYTHONDEVMODE") == "1" + + +@asynccontextmanager +async def debug_query(sql_query: str, query_params: dict | None = None): + """Time the processing time of an sql query.""" + if not ENABLE_DEBUG: + yield + return + time_start = time.time() + try: + yield + except OperationalError as err: + LOGGER.error(f"{err}\n{sql_query}") + raise + finally: + process_time = time.time() - time_start + if process_time > 0.5: + # log slow queries + for key, value in (query_params or {}).items(): + sql_query = sql_query.replace(f":{key}", repr(value)) + LOGGER.warning("SQL Query took %s seconds! (\n%s\n", process_time, sql_query) + + +def query_params(query: str, params: dict[str, Any] | None) -> tuple[str, dict[str, Any]]: + """Extend query parameters support.""" + if params is None: + return (query, params) + count = 0 + result_query = query + result_params = {} + for key, value in params.items(): + # add support for a list within the query params + # recreates the params as (:_param_0, :_param_1) etc + if isinstance(value, list | tuple): + subparams = [] + for subval in value: + subparam_name = f"_param_{count}" + result_params[subparam_name] = subval + subparams.append(subparam_name) + count += 1 + params_str = ",".join(f":{x}" for x in subparams) + result_query = result_query.replace(f" :{key}", f" ({params_str})") + else: + result_params[key] = params[key] + return (result_query, result_params) + + +class DatabaseConnection: + """Class that holds the (connection to the) database with some convenience helper functions.""" + + _db: aiosqlite.Connection + + def __init__(self, db_path: str) -> None: + """Initialize class.""" + self.db_path = db_path + + async def setup(self) -> None: + """Perform async initialization.""" + self._db = await aiosqlite.connect(self.db_path) + self._db.row_factory = aiosqlite.Row + await self.execute("PRAGMA analysis_limit=10000;") + await self.execute("PRAGMA optimize;") + await self.commit() + + async def close(self) -> None: + """Close db connection on exit.""" + await self.execute("PRAGMA optimize;") + await self.commit() + await self._db.close() + + async def get_rows( + self, + table: str, + match: dict | None = None, + order_by: str | None = None, + limit: int = 500, + offset: int = 0, + ) -> list[Mapping]: + """Get all rows for given table.""" + sql_query = f"SELECT * FROM {table}" + if match is not None: + sql_query += " WHERE " + " AND ".join(f"{x} = :{x}" for x in match) + if order_by is not None: + sql_query += f" ORDER BY {order_by}" + if limit: + sql_query += f" LIMIT {limit} OFFSET {offset}" + async with debug_query(sql_query): + return await self._db.execute_fetchall(sql_query, match) + + async def get_rows_from_query( + self, + query: str, + params: dict | None = None, + limit: int = 500, + offset: int = 0, + ) -> list[Mapping]: + """Get all rows for given custom query.""" + if limit: + query += f" LIMIT {limit} OFFSET {offset}" + _query, _params = query_params(query, params) + async with debug_query(_query, _params): + return await self._db.execute_fetchall(_query, _params) + + async def get_count_from_query( + self, + query: str, + params: dict | None = None, + ) -> int: + """Get row count for given custom query.""" + query = f"SELECT count() FROM ({query})" + _query, _params = query_params(query, params) + async with debug_query(_query): + async with self._db.execute(_query, _params) as cursor: + if result := await cursor.fetchone(): + return result[0] + return 0 + + async def get_count( + self, + table: str, + ) -> int: + """Get row count for given table.""" + query = f"SELECT count(*) FROM {table}" + async with debug_query(query): + async with self._db.execute(query) as cursor: + if result := await cursor.fetchone(): + return result[0] + return 0 + + async def search(self, table: str, search: str, column: str = "name") -> list[Mapping]: + """Search table by column.""" + sql_query = f"SELECT * FROM {table} WHERE {table}.{column} LIKE :search" + params = {"search": f"%{search}%"} + async with debug_query(sql_query, params): + return await self._db.execute_fetchall(sql_query, params) + + async def get_row(self, table: str, match: dict[str, Any]) -> Mapping | None: + """Get single row for given table where column matches keys/values.""" + sql_query = f"SELECT * FROM {table} WHERE " + sql_query += " AND ".join(f"{table}.{x} = :{x}" for x in match) + async with debug_query(sql_query, match), self._db.execute(sql_query, match) as cursor: + return await cursor.fetchone() + + async def insert( + self, + table: str, + values: dict[str, Any], + allow_replace: bool = False, + ) -> int: + """Insert data in given table.""" + keys = tuple(values.keys()) + if allow_replace: + sql_query = f'INSERT OR REPLACE INTO {table}({",".join(keys)})' + else: + sql_query = f'INSERT INTO {table}({",".join(keys)})' + sql_query += f' VALUES ({",".join(f":{x}" for x in keys)})' + row_id = await self._db.execute_insert(sql_query, values) + await self._db.commit() + return row_id[0] + + async def insert_or_replace(self, table: str, values: dict[str, Any]) -> Mapping: + """Insert or replace data in given table.""" + return await self.insert(table=table, values=values, allow_replace=True) + + async def update( + self, + table: str, + match: dict[str, Any], + values: dict[str, Any], + ) -> Mapping: + """Update record.""" + keys = tuple(values.keys()) + sql_query = f'UPDATE {table} SET {",".join(f"{x}=:{x}" for x in keys)} WHERE ' + sql_query += " AND ".join(f"{x} = :{x}" for x in match) + await self.execute(sql_query, {**match, **values}) + await self._db.commit() + # return updated item + return await self.get_row(table, match) + + async def delete(self, table: str, match: dict | None = None, query: str | None = None) -> None: + """Delete data in given table.""" + assert not (query and "where" in query.lower()) + sql_query = f"DELETE FROM {table} " + if match: + sql_query += " WHERE " + " AND ".join(f"{x} = :{x}" for x in match) + elif query and "where" not in query.lower(): + sql_query += "WHERE " + query + elif query: + sql_query += query + await self.execute(sql_query, match) + await self._db.commit() + + async def delete_where_query(self, table: str, query: str | None = None) -> None: + """Delete data in given table using given where clausule.""" + sql_query = f"DELETE FROM {table} WHERE {query}" + await self.execute(sql_query) + await self._db.commit() + + async def execute(self, query: str, values: dict | None = None) -> Any: + """Execute command on the database.""" + return await self._db.execute(query, values) + + async def commit(self) -> None: + """Commit the current transaction.""" + return await self._db.commit() + + async def iter_items( + self, + table: str, + match: dict | None = None, + ) -> AsyncGenerator[Mapping, None]: + """Iterate all items within a table.""" + limit: int = 500 + offset: int = 0 + while True: + next_items = await self.get_rows( + table=table, + match=match, + offset=offset, + limit=limit, + ) + for item in next_items: + yield item + if len(next_items) < limit: + break + await asyncio.sleep(0) # yield to eventloop + offset += limit + + async def vacuum(self) -> None: + """Run vacuum command on database.""" + await self._db.execute("VACUUM") + await self._db.commit() diff --git a/music_assistant/helpers/datetime.py b/music_assistant/helpers/datetime.py new file mode 100644 index 00000000..af8f4b24 --- /dev/null +++ b/music_assistant/helpers/datetime.py @@ -0,0 +1,47 @@ +"""Helpers for date and time.""" + +from __future__ import annotations + +import datetime + +LOCAL_TIMEZONE = datetime.datetime.now(datetime.UTC).astimezone().tzinfo + + +def utc() -> datetime.datetime: + """Get current UTC datetime.""" + return datetime.datetime.now(datetime.UTC) + + +def utc_timestamp() -> float: + """Return UTC timestamp in seconds as float.""" + return utc().timestamp() + + +def now() -> datetime.datetime: + """Get current datetime in local timezone.""" + return datetime.datetime.now(LOCAL_TIMEZONE) + + +def now_timestamp() -> float: + """Return current datetime as timestamp in local timezone.""" + return now().timestamp() + + +def future_timestamp(**kwargs: float) -> float: + """Return current timestamp + timedelta.""" + return (now() + datetime.timedelta(**kwargs)).timestamp() + + +def from_utc_timestamp(timestamp: float) -> datetime.datetime: + """Return datetime from UTC timestamp.""" + return datetime.datetime.fromtimestamp(timestamp, datetime.UTC) + + +def iso_from_utc_timestamp(timestamp: float) -> str: + """Return ISO 8601 datetime string from UTC timestamp.""" + return from_utc_timestamp(timestamp).isoformat() + + +def from_iso_string(iso_datetime: str) -> datetime.datetime: + """Return datetime from ISO datetime string.""" + return datetime.datetime.fromisoformat(iso_datetime) diff --git a/music_assistant/helpers/didl_lite.py b/music_assistant/helpers/didl_lite.py new file mode 100644 index 00000000..78ce38c7 --- /dev/null +++ b/music_assistant/helpers/didl_lite.py @@ -0,0 +1,61 @@ +"""Helper(s) to create DIDL Lite metadata for Sonos/DLNA players.""" + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +from music_assistant_models.enums import MediaType + +from music_assistant.constants import MASS_LOGO_ONLINE + +if TYPE_CHECKING: + from music_assistant_models.player import PlayerMedia + +# ruff: noqa: E501 + + +def create_didl_metadata(media: PlayerMedia) -> str: + """Create DIDL metadata string from url and PlayerMedia.""" + ext = media.uri.split(".")[-1].split("?")[0] + image_url = media.image_url or MASS_LOGO_ONLINE + if media.media_type in (MediaType.FLOW_STREAM, MediaType.RADIO) or not media.duration: + # flow stream, radio or other duration-less stream + title = media.title or media.uri + return ( + '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">' + f'<item id="flowmode" parentID="0" restricted="1">' + f"<dc:title>{escape_string(title)}</dc:title>" + f"<upnp:albumArtURI>{escape_string(image_url)}</upnp:albumArtURI>" + f"<dc:queueItemId>{media.uri}</dc:queueItemId>" + "<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>" + f"<upnp:mimeType>audio/{ext}</upnp:mimeType>" + f'<res duration="23:59:59.000" protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{escape_string(media.uri)}</res>' + "</item>" + "</DIDL-Lite>" + ) + duration_str = str(datetime.timedelta(seconds=media.duration or 0)) + ".000" + return ( + '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">' + '<item id="1" parentID="0" restricted="1">' + f"<dc:title>{escape_string(media.title or media.uri)}</dc:title>" + f"<dc:creator>{escape_string(media.artist or '')}</dc:creator>" + f"<upnp:album>{escape_string(media.album or '')}</upnp:album>" + f"<upnp:artist>{escape_string(media.artist or '')}</upnp:artist>" + f"<upnp:duration>{int(media.duration or 0)}</upnp:duration>" + f"<dc:queueItemId>{media.uri}</dc:queueItemId>" + f"<upnp:albumArtURI>{escape_string(image_url)}</upnp:albumArtURI>" + "<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>" + f"<upnp:mimeType>audio/{ext}</upnp:mimeType>" + f'<res duration="{duration_str}" protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{escape_string(media.uri)}</res>' + "</item>" + "</DIDL-Lite>" + ) + + +def escape_string(data: str) -> str: + """Create DIDL-safe string.""" + data = data.replace("&", "&") + # data = data.replace("?", "?") + data = data.replace(">", ">") + return data.replace("<", "<") diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py new file mode 100644 index 00000000..1cf91158 --- /dev/null +++ b/music_assistant/helpers/ffmpeg.py @@ -0,0 +1,324 @@ +"""FFMpeg related helpers.""" + +from __future__ import annotations + +import asyncio +import logging +from collections import deque +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING + +from music_assistant_models.errors import AudioError +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.media_items import AudioFormat, ContentType + +from music_assistant.constants import VERBOSE_LOG_LEVEL + +from .process import AsyncProcess +from .util import TimedAsyncGenerator, close_async_generator + +LOGGER = logging.getLogger("ffmpeg") +MINIMAL_FFMPEG_VERSION = 6 + + +class FFMpeg(AsyncProcess): + """FFMpeg wrapped as AsyncProcess.""" + + def __init__( + self, + audio_input: AsyncGenerator[bytes, None] | str | int, + input_format: AudioFormat, + output_format: AudioFormat, + filter_params: list[str] | None = None, + extra_args: list[str] | None = None, + extra_input_args: list[str] | None = None, + audio_output: str | int = "-", + collect_log_history: bool = False, + ) -> None: + """Initialize AsyncProcess.""" + ffmpeg_args = get_ffmpeg_args( + input_format=input_format, + output_format=output_format, + filter_params=filter_params or [], + extra_args=extra_args or [], + input_path=audio_input if isinstance(audio_input, str) else "-", + output_path=audio_output if isinstance(audio_output, str) else "-", + extra_input_args=extra_input_args or [], + loglevel="info", + ) + self.audio_input = audio_input + self.input_format = input_format + self.collect_log_history = collect_log_history + self.log_history: deque[str] = deque(maxlen=100) + self._stdin_task: asyncio.Task | None = None + self._logger_task: asyncio.Task | None = None + super().__init__( + ffmpeg_args, + stdin=True if isinstance(audio_input, str | AsyncGenerator) else audio_input, + stdout=True if isinstance(audio_output, str) else audio_output, + stderr=True, + ) + self.logger = LOGGER + + async def start(self) -> None: + """Perform Async init of process.""" + await super().start() + if self.proc: + self.logger = LOGGER.getChild(str(self.proc.pid)) + clean_args = [] + for arg in self._args[1:]: + if arg.startswith("http"): + clean_args.append("<URL>") + elif "/" in arg and "." in arg: + clean_args.append("<FILE>") + else: + clean_args.append(arg) + args_str = " ".join(clean_args) + self.logger.log(VERBOSE_LOG_LEVEL, "started with args: %s", args_str) + self._logger_task = asyncio.create_task(self._log_reader_task()) + if isinstance(self.audio_input, AsyncGenerator): + self._stdin_task = asyncio.create_task(self._feed_stdin()) + + async def close(self, send_signal: bool = True) -> None: + """Close/terminate the process and wait for exit.""" + if self.closed: + return + if self._stdin_task and not self._stdin_task.done(): + self._stdin_task.cancel() + await super().close(send_signal) + + async def _log_reader_task(self) -> None: + """Read ffmpeg log from stderr.""" + decode_errors = 0 + async for line in self.iter_stderr(): + if self.collect_log_history: + self.log_history.append(line) + if "error" in line or "warning" in line: + self.logger.debug(line) + elif "critical" in line: + self.logger.warning(line) + else: + self.logger.log(VERBOSE_LOG_LEVEL, line) + + if "Invalid data found when processing input" in line: + decode_errors += 1 + if decode_errors >= 50: + self.logger.error(line) + await super().close(True) + + # if streamdetails contenttype is unknown, try parse it from the ffmpeg log + if line.startswith("Stream #") and ": Audio: " in line: + if self.input_format.content_type == ContentType.UNKNOWN: + content_type_raw = line.split(": Audio: ")[1].split(" ")[0] + content_type = ContentType.try_parse(content_type_raw) + self.logger.debug( + "Detected (input) content type: %s (%s)", content_type, content_type_raw + ) + self.input_format.content_type = content_type + del line + + async def _feed_stdin(self) -> None: + """Feed stdin with audio chunks from an AsyncGenerator.""" + if TYPE_CHECKING: + self.audio_input: AsyncGenerator[bytes, None] + generator_exhausted = False + audio_received = False + try: + async for chunk in TimedAsyncGenerator(self.audio_input, 300): + audio_received = True + if self.proc and self.proc.returncode is not None: + raise AudioError("Parent process already exited") + await self.write(chunk) + generator_exhausted = True + if not audio_received: + raise AudioError("No audio data received from source") + except Exception as err: + if isinstance(err, asyncio.CancelledError): + return + self.logger.error( + "Stream error: %s", + str(err) or err.__class__.__name__, + exc_info=err if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else None, + ) + finally: + await self.write_eof() + # we need to ensure that we close the async generator + # if we get cancelled otherwise it keeps lingering forever + if not generator_exhausted: + await close_async_generator(self.audio_input) + + +async def get_ffmpeg_stream( + audio_input: AsyncGenerator[bytes, None] | str, + input_format: AudioFormat, + output_format: AudioFormat, + filter_params: list[str] | None = None, + extra_args: list[str] | None = None, + chunk_size: int | None = None, + extra_input_args: list[str] | None = None, +) -> AsyncGenerator[bytes, None]: + """ + Get the ffmpeg audio stream as async generator. + + Takes care of resampling and/or recoding if needed, + according to player preferences. + """ + async with FFMpeg( + audio_input=audio_input, + input_format=input_format, + output_format=output_format, + filter_params=filter_params, + extra_args=extra_args, + extra_input_args=extra_input_args, + ) as ffmpeg_proc: + # read final chunks from stdout + iterator = ffmpeg_proc.iter_chunked(chunk_size) if chunk_size else ffmpeg_proc.iter_any() + async for chunk in iterator: + yield chunk + + +def get_ffmpeg_args( # noqa: PLR0915 + input_format: AudioFormat, + output_format: AudioFormat, + filter_params: list[str], + extra_args: list[str] | None = None, + input_path: str = "-", + output_path: str = "-", + extra_input_args: list[str] | None = None, + loglevel: str = "error", +) -> list[str]: + """Collect all args to send to the ffmpeg process.""" + if extra_args is None: + extra_args = [] + ffmpeg_present, libsoxr_support, version = get_global_cache_value("ffmpeg_support") + if not ffmpeg_present: + msg = ( + "FFmpeg binary is missing from system." + "Please install ffmpeg on your OS to enable playback." + ) + raise AudioError( + msg, + ) + + major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) + if major_version < MINIMAL_FFMPEG_VERSION: + msg = ( + f"FFmpeg version {version} is not supported. " + f"Minimal version required is {MINIMAL_FFMPEG_VERSION}." + ) + raise AudioError(msg) + + # generic args + generic_args = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + loglevel, + "-nostats", + "-ignore_unknown", + "-protocol_whitelist", + "file,hls,http,https,tcp,tls,crypto,pipe,data,fd,rtp,udp", + ] + # collect input args + input_args = [] + if extra_input_args: + input_args += extra_input_args + if input_path.startswith("http"): + # append reconnect options for direct stream from http + input_args += [ + # Reconnect automatically when disconnected before EOF is hit. + "-reconnect", + "1", + # Set the maximum delay in seconds after which to give up reconnecting. + "-reconnect_delay_max", + "30", + # If set then even streamed/non seekable streams will be reconnected on errors. + "-reconnect_streamed", + "1", + # Reconnect automatically in case of TCP/TLS errors during connect. + "-reconnect_on_network_error", + "1", + # A comma separated list of HTTP status codes to reconnect on. + # The list can include specific status codes (e.g. 503) or the strings 4xx / 5xx. + "-reconnect_on_http_error", + "5xx,4xx", + ] + if input_format.content_type.is_pcm(): + input_args += [ + "-ac", + str(input_format.channels), + "-channel_layout", + "mono" if input_format.channels == 1 else "stereo", + "-ar", + str(input_format.sample_rate), + "-acodec", + input_format.content_type.name.lower(), + "-f", + input_format.content_type.value, + "-i", + input_path, + ] + else: + # let ffmpeg auto detect the content type from the metadata/headers + input_args += ["-i", input_path] + + # collect output args + output_args = [] + if output_path.upper() == "NULL": + # devnull stream + output_args = ["-f", "null", "-"] + elif output_format.content_type == ContentType.UNKNOWN: + raise RuntimeError("Invalid output format specified") + elif output_format.content_type == ContentType.AAC: + output_args = ["-f", "adts", "-c:a", "aac", "-b:a", "256k", output_path] + elif output_format.content_type == ContentType.MP3: + output_args = ["-f", "mp3", "-b:a", "320k", output_path] + else: + if output_format.content_type.is_pcm(): + output_args += ["-acodec", output_format.content_type.name.lower()] + # use explicit format identifier for all other + output_args += [ + "-f", + output_format.content_type.value, + "-ar", + str(output_format.sample_rate), + "-ac", + str(output_format.channels), + ] + if output_format.output_format_str == "flac": + # use level 0 compression for fastest encoding + output_args += ["-compression_level", "0"] + output_args += [output_path] + + # edge case: source file is not stereo - downmix to stereo + if input_format.channels > 2 and output_format.channels == 2: + filter_params = [ + "pan=stereo|FL=1.0*FL+0.707*FC+0.707*SL+0.707*LFE|FR=1.0*FR+0.707*FC+0.707*SR+0.707*LFE", + *filter_params, + ] + + # determine if we need to do resampling + if ( + input_format.sample_rate != output_format.sample_rate + or input_format.bit_depth > output_format.bit_depth + ): + # prefer resampling with libsoxr due to its high quality + if libsoxr_support: + resample_filter = "aresample=resampler=soxr:precision=30" + else: + resample_filter = "aresample=resampler=swr" + + # sample rate conversion + if input_format.sample_rate != output_format.sample_rate: + resample_filter += f":osr={output_format.sample_rate}" + + # bit depth conversion: apply dithering when going down to 16 bits + if output_format.bit_depth == 16 and input_format.bit_depth > 16: + resample_filter += ":osf=s16:dither_method=triangular_hp" + + filter_params.append(resample_filter) + + if filter_params and "-filter_complex" not in extra_args: + extra_args += ["-af", ",".join(filter_params)] + + return generic_args + input_args + extra_args + output_args diff --git a/music_assistant/helpers/global_cache.py b/music_assistant/helpers/global_cache.py new file mode 100644 index 00000000..6cd741dd --- /dev/null +++ b/music_assistant/helpers/global_cache.py @@ -0,0 +1,28 @@ +"""Provides a simple global memory cache.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +# global cache - we use this on a few places (as limited as possible) +# where we have no other options +_global_cache_lock = asyncio.Lock() +_global_cache: dict[str, Any] = {} + + +def get_global_cache_value(key: str, default: Any = None) -> Any: + """Get a value from the global cache.""" + return _global_cache.get(key, default) + + +async def set_global_cache_values(values: dict[str, Any]) -> Any: + """Set a value in the global cache (without locking).""" + async with _global_cache_lock: + for key, value in values.items(): + _set_global_cache_value(key, value) + + +def _set_global_cache_value(key: str, value: Any) -> Any: + """Set a value in the global cache (without locking).""" + _global_cache[key] = value diff --git a/music_assistant/helpers/images.py b/music_assistant/helpers/images.py new file mode 100644 index 00000000..99e34c40 --- /dev/null +++ b/music_assistant/helpers/images.py @@ -0,0 +1,139 @@ +"""Utilities for image manipulation and retrieval.""" + +from __future__ import annotations + +import asyncio +import itertools +import os +import random +from base64 import b64decode +from collections.abc import Iterable +from io import BytesIO +from typing import TYPE_CHECKING + +import aiofiles +from aiohttp.client_exceptions import ClientError +from PIL import Image, UnidentifiedImageError + +from music_assistant.helpers.tags import get_embedded_image +from music_assistant.models.metadata_provider import MetadataProvider + +if TYPE_CHECKING: + from music_assistant_models.media_items import MediaItemImage + + from music_assistant import MusicAssistant + from music_assistant.models.music_provider import MusicProvider + + +async def get_image_data(mass: MusicAssistant, path_or_url: str, provider: str) -> bytes: + """Create thumbnail from image url.""" + # TODO: add local cache here ! + if prov := mass.get_provider(provider): + prov: MusicProvider | MetadataProvider + if resolved_image := await prov.resolve_image(path_or_url): + if isinstance(resolved_image, bytes): + return resolved_image + if isinstance(resolved_image, str): + path_or_url = resolved_image + # handle HTTP location + if path_or_url.startswith("http"): + try: + async with mass.http_session.get(path_or_url, raise_for_status=True) as resp: + return await resp.read() + except ClientError as err: + raise FileNotFoundError from err + # handle base64 embedded images + if path_or_url.startswith("data:image"): + return b64decode(path_or_url.split(",")[-1]) + # handle FILE location (of type image) + if path_or_url.endswith(("jpg", "JPG", "png", "PNG", "jpeg")): + if await asyncio.to_thread(os.path.isfile, path_or_url): + async with aiofiles.open(path_or_url, "rb") as _file: + return await _file.read() + # use ffmpeg for embedded images + if img_data := await get_embedded_image(path_or_url): + return img_data + msg = f"Image not found: {path_or_url}" + raise FileNotFoundError(msg) + + +async def get_image_thumb( + mass: MusicAssistant, + path_or_url: str, + size: int | None, + provider: str, + image_format: str = "PNG", +) -> bytes: + """Get (optimized) PNG thumbnail from image url.""" + img_data = await get_image_data(mass, path_or_url, provider) + if not img_data or not isinstance(img_data, bytes): + raise FileNotFoundError(f"Image not found: {path_or_url}") + + if not size and image_format.encode() in img_data: + return img_data + + def _create_image(): + data = BytesIO() + try: + img = Image.open(BytesIO(img_data)) + except UnidentifiedImageError: + raise FileNotFoundError(f"Invalid image: {path_or_url}") + if size: + img.thumbnail((size, size), Image.Resampling.LANCZOS) + + mode = "RGBA" if image_format == "PNG" else "RGB" + img.convert(mode).save(data, image_format, optimize=True) + return data.getvalue() + + image_format = image_format.upper() + return await asyncio.to_thread(_create_image) + + +async def create_collage( + mass: MusicAssistant, images: Iterable[MediaItemImage], dimensions: tuple[int] = (1500, 1500) +) -> bytes: + """Create a basic collage image from multiple image urls.""" + image_size = 250 + + def _new_collage(): + return Image.new("RGB", (dimensions[0], dimensions[1]), color=(255, 255, 255, 255)) + + collage = await asyncio.to_thread(_new_collage) + + def _add_to_collage(img_data: bytes, coord_x: int, coord_y: int) -> None: + data = BytesIO(img_data) + photo = Image.open(data).convert("RGB") + photo = photo.resize((image_size, image_size)) + collage.paste(photo, (coord_x, coord_y)) + del data + + # prevent duplicates with a set + images = list(set(images)) + random.shuffle(images) + iter_images = itertools.cycle(images) + + for x_co in range(0, dimensions[0], image_size): + for y_co in range(0, dimensions[1], image_size): + for _ in range(5): + img = next(iter_images) + img_data = await get_image_data(mass, img.path, img.provider) + if img_data: + await asyncio.to_thread(_add_to_collage, img_data, x_co, y_co) + del img_data + break + + def _save_collage(): + final_data = BytesIO() + collage.convert("RGB").save(final_data, "JPEG", optimize=True) + return final_data.getvalue() + + return await asyncio.to_thread(_save_collage) + + +async def get_icon_string(icon_path: str) -> str: + """Get svg icon as string.""" + ext = icon_path.rsplit(".")[-1] + assert ext == "svg" + async with aiofiles.open(icon_path) as _file: + xml_data = await _file.read() + return xml_data.replace("\n", "").strip() diff --git a/music_assistant/helpers/json.py b/music_assistant/helpers/json.py new file mode 100644 index 00000000..49b85ba7 --- /dev/null +++ b/music_assistant/helpers/json.py @@ -0,0 +1,70 @@ +"""Helpers to work with (de)serializing of json.""" + +import asyncio +import base64 +from _collections_abc import dict_keys, dict_values +from types import MethodType +from typing import Any, TypeVar + +import aiofiles +import orjson +from mashumaro.mixins.orjson import DataClassORJSONMixin + +JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) +JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,) + +DO_NOT_SERIALIZE_TYPES = (MethodType, asyncio.Task) + + +def get_serializable_value(obj: Any, raise_unhandled: bool = False) -> Any: + """Parse the value to its serializable equivalent.""" + if getattr(obj, "do_not_serialize", None): + return None + if ( + isinstance(obj, list | set | filter | tuple | dict_values | dict_keys | dict_values) + or obj.__class__ == "dict_valueiterator" + ): + return [get_serializable_value(x) for x in obj] + if hasattr(obj, "to_dict"): + return obj.to_dict() + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("ascii") + if isinstance(obj, DO_NOT_SERIALIZE_TYPES): + return None + if raise_unhandled: + raise TypeError + return obj + + +def serialize_to_json(obj: Any) -> Any: + """Serialize a value (or a list of values) to json.""" + if obj is None: + return obj + if hasattr(obj, "to_json"): + return obj.to_json() + return json_dumps(get_serializable_value(obj)) + + +def json_dumps(data: Any, indent: bool = False) -> str: + """Dump json string.""" + # we use the passthrough dataclass option because we use mashumaro for that + option = orjson.OPT_OMIT_MICROSECONDS | orjson.OPT_PASSTHROUGH_DATACLASS + if indent: + option |= orjson.OPT_INDENT_2 + return orjson.dumps( + data, + default=get_serializable_value, + option=option, + ).decode("utf-8") + + +json_loads = orjson.loads + +TargetT = TypeVar("TargetT", bound=DataClassORJSONMixin) + + +async def load_json_file(path: str, target_class: type[TargetT]) -> TargetT: + """Load JSON from file.""" + async with aiofiles.open(path) as _file: + content = await _file.read() + return target_class.from_json(content) diff --git a/music_assistant/helpers/logging.py b/music_assistant/helpers/logging.py new file mode 100644 index 00000000..1dac5a3a --- /dev/null +++ b/music_assistant/helpers/logging.py @@ -0,0 +1,193 @@ +""" +Logging utilities. + +A lot in this file has been copied from Home Assistant: +https://github.com/home-assistant/core/blob/e5ccd85e7e26c167d0b73669a88bc3a7614dd456/homeassistant/util/logging.py#L78 + +All rights reserved. +""" + +from __future__ import annotations + +import asyncio +import inspect +import logging +import logging.handlers +import queue +import traceback +from collections.abc import Callable, Coroutine +from functools import partial, wraps +from typing import Any, TypeVar, cast, overload + +_T = TypeVar("_T") + + +class LoggingQueueHandler(logging.handlers.QueueHandler): + """Process the log in another thread.""" + + listener: logging.handlers.QueueListener | None = None + + def prepare(self, record: logging.LogRecord) -> logging.LogRecord: + """Prepare a record for queuing. + + This is added as a workaround for https://bugs.python.org/issue46755 + """ + record = super().prepare(record) + record.stack_info = None + return record + + def handle(self, record: logging.LogRecord) -> Any: + """Conditionally emit the specified logging record. + + Depending on which filters have been added to the handler, push the new + records onto the backing Queue. + + The default python logger Handler acquires a lock + in the parent class which we do not need as + SimpleQueue is already thread safe. + + See https://bugs.python.org/issue24645 + """ + return_value = self.filter(record) + if return_value: + self.emit(record) + return return_value + + def close(self) -> None: + """Tidy up any resources used by the handler. + + This adds shutdown of the QueueListener + """ + super().close() + if not self.listener: + return + self.listener.stop() + self.listener = None + + +def activate_log_queue_handler() -> None: + """Migrate the existing log handlers to use the queue. + + This allows us to avoid blocking I/O and formatting messages + in the event loop as log messages are written in another thread. + """ + simple_queue: queue.SimpleQueue[logging.Handler] = queue.SimpleQueue() + queue_handler = LoggingQueueHandler(simple_queue) + logging.root.addHandler(queue_handler) + + migrated_handlers: list[logging.Handler] = [] + for handler in logging.root.handlers[:]: + if handler is queue_handler: + continue + logging.root.removeHandler(handler) + migrated_handlers.append(handler) + + listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers) + queue_handler.listener = listener + + listener.start() + + +def log_exception(format_err: Callable[..., Any], *args: Any) -> None: + """Log an exception with additional context.""" + module = inspect.getmodule(inspect.stack(context=0)[1].frame) + if module is not None: # noqa: SIM108 + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/core/issues/24982 + module_name = __name__ + + # Do not print the wrapper in the traceback + frames = len(inspect.trace()) - 1 + exc_msg = traceback.format_exc(-frames) + friendly_msg = format_err(*args) + logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) + + +@overload +def catch_log_exception( + func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] +) -> Callable[..., Coroutine[Any, Any, None]]: ... + + +@overload +def catch_log_exception( + func: Callable[..., Any], format_err: Callable[..., Any] +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: ... + + +def catch_log_exception( + func: Callable[..., Any], format_err: Callable[..., Any] +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: + """Decorate a function func to catch and log exceptions. + + If func is a coroutine function, a coroutine function will be returned. + If func is a callback, a callback will be returned. + """ + # Check for partials to properly determine if coroutine function + check_func = func + while isinstance(check_func, partial): + check_func = check_func.func + + wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] + if asyncio.iscoroutinefunction(check_func): + async_func = cast(Callable[..., Coroutine[Any, Any, None]], func) + + @wraps(async_func) + async def async_wrapper(*args: Any) -> None: + """Catch and log exception.""" + try: + await async_func(*args) + except Exception: + log_exception(format_err, *args) + + wrapper_func = async_wrapper + + else: + + @wraps(func) + def wrapper(*args: Any) -> None: + """Catch and log exception.""" + try: + func(*args) + except Exception: + log_exception(format_err, *args) + + wrapper_func = wrapper + return wrapper_func + + +def catch_log_coro_exception( + target: Coroutine[Any, Any, _T], format_err: Callable[..., Any], *args: Any +) -> Coroutine[Any, Any, _T | None]: + """Decorate a coroutine to catch and log exceptions.""" + + async def coro_wrapper(*args: Any) -> _T | None: + """Catch and log exception.""" + try: + return await target + except Exception: + log_exception(format_err, *args) + return None + + return coro_wrapper(*args) + + +def async_create_catching_coro(target: Coroutine[Any, Any, _T]) -> Coroutine[Any, Any, _T | None]: + """Wrap a coroutine to catch and log exceptions. + + The exception will be logged together with a stacktrace of where the + coroutine was wrapped. + + target: target coroutine. + """ + trace = traceback.extract_stack() + return catch_log_coro_exception( + target, + lambda: "Exception in {} called from\n {}".format( + target.__name__, + "".join(traceback.format_list(trace[:-1])), + ), + ) diff --git a/music_assistant/helpers/playlists.py b/music_assistant/helpers/playlists.py new file mode 100644 index 00000000..9d72381b --- /dev/null +++ b/music_assistant/helpers/playlists.py @@ -0,0 +1,179 @@ +"""Helpers for parsing (online and offline) playlists.""" + +from __future__ import annotations + +import configparser +import logging +from dataclasses import dataclass +from typing import TYPE_CHECKING +from urllib.parse import urlparse + +from aiohttp import client_exceptions +from music_assistant_models.errors import InvalidDataError + +from music_assistant.helpers.util import detect_charset + +if TYPE_CHECKING: + from music_assistant import MusicAssistant + + +LOGGER = logging.getLogger(__name__) +HLS_CONTENT_TYPES = ( + # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 + "application/vnd.apple.mpegurl", + # Additional informal types used by Mozilla gecko not included as they + # don't reliably indicate HLS streams +) + + +class IsHLSPlaylist(InvalidDataError): + """The playlist from an HLS stream and should not be parsed.""" + + +@dataclass +class PlaylistItem: + """Playlist item.""" + + path: str + length: str | None = None + title: str | None = None + stream_info: dict[str, str] | None = None + key: str | None = None + + @property + def is_url(self) -> bool: + """Validate the URL can be parsed and at least has scheme + netloc.""" + result = urlparse(self.path) + return all([result.scheme, result.netloc]) + + +def parse_m3u(m3u_data: str) -> list[PlaylistItem]: + """Very simple m3u parser. + + Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py + """ + # From Mozilla gecko source: https://github.com/mozilla/gecko-dev/blob/c4c1adbae87bf2d128c39832d72498550ee1b4b8/dom/media/DecoderTraits.cpp#L47-L52 + + m3u_lines = m3u_data.splitlines() + + playlist = [] + + length = None + title = None + stream_info = None + key = None + + for line in m3u_lines: + line = line.strip() # noqa: PLW2901 + if line.startswith("#EXTINF:"): + # Get length and title from #EXTINF line + info = line.split("#EXTINF:")[1].split(",", 1) + if len(info) != 2: + continue + length = info[0].strip()[0] + if length == "-1": + length = None + title = info[1].strip() + elif line.startswith("#EXT-X-STREAM-INF:"): + # HLS stream properties + # https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-10 + stream_info = {} + for part in line.replace("#EXT-X-STREAM-INF:", "").split(","): + if "=" not in part: + continue + kev_value_parts = part.strip().split("=") + stream_info[kev_value_parts[0]] = kev_value_parts[1] + elif line.startswith("#EXT-X-KEY:"): + key = line.split(",URI=")[1].strip('"') + elif line.startswith("#"): + # Ignore other extensions + continue + elif len(line) != 0: + filepath = line + if "%20" in filepath: + # apparently VLC manages to encode spaces in filenames + filepath = filepath.replace("%20", " ") + # replace Windows directory separators + filepath = filepath.replace("\\", "/") + playlist.append( + PlaylistItem( + path=filepath, length=length, title=title, stream_info=stream_info, key=key + ) + ) + # reset the song variables so it doesn't use the same EXTINF more than once + length = None + title = None + stream_info = None + + return playlist + + +def parse_pls(pls_data: str) -> list[PlaylistItem]: + """Parse (only) filenames/urls from pls playlist file.""" + pls_parser = configparser.ConfigParser() + try: + pls_parser.read_string(pls_data, "playlist") + except configparser.Error as err: + raise InvalidDataError("Can't parse playlist") from err + + if "playlist" not in pls_parser or pls_parser["playlist"].getint("Version") != 2: + raise InvalidDataError("Invalid playlist") + + try: + num_entries = pls_parser.getint("playlist", "NumberOfEntries") + except (configparser.NoOptionError, ValueError) as err: + raise InvalidDataError("Invalid NumberOfEntries in playlist") from err + + playlist_section = pls_parser["playlist"] + + playlist = [] + for entry in range(1, num_entries + 1): + file_option = f"File{entry}" + if file_option not in playlist_section: + continue + itempath = playlist_section[file_option] + length = playlist_section.get(f"Length{entry}") + playlist.append( + PlaylistItem( + length=length if length and length != "-1" else None, + title=playlist_section.get(f"Title{entry}"), + path=itempath, + ) + ) + return playlist + + +async def fetch_playlist( + mass: MusicAssistant, url: str, raise_on_hls: bool = True +) -> list[PlaylistItem]: + """Parse an online m3u or pls playlist.""" + try: + async with mass.http_session.get(url, allow_redirects=True, timeout=5) as resp: + try: + raw_data = await resp.content.read(64 * 1024) + # NOTE: using resp.charset is not reliable, we need to detect it ourselves + encoding = resp.charset or await detect_charset(raw_data) + playlist_data = raw_data.decode(encoding, errors="replace") + except (ValueError, UnicodeDecodeError) as err: + msg = f"Could not decode playlist {url}" + raise InvalidDataError(msg) from err + except TimeoutError as err: + msg = f"Timeout while fetching playlist {url}" + raise InvalidDataError(msg) from err + except client_exceptions.ClientError as err: + msg = f"Error while fetching playlist {url}" + raise InvalidDataError(msg) from err + + if raise_on_hls and "#EXT-X-VERSION:" in playlist_data or "#EXT-X-STREAM-INF:" in playlist_data: + raise IsHLSPlaylist + + if url.endswith((".m3u", ".m3u8")): + playlist = parse_m3u(playlist_data) + else: + playlist = parse_pls(playlist_data) + + if not playlist: + msg = f"Empty playlist {url}" + raise InvalidDataError(msg) + + return playlist diff --git a/music_assistant/helpers/process.py b/music_assistant/helpers/process.py new file mode 100644 index 00000000..21d0ef03 --- /dev/null +++ b/music_assistant/helpers/process.py @@ -0,0 +1,282 @@ +""" +AsyncProcess. + +Wrapper around asyncio subprocess to help with using pipe streams and +taking care of properly closing the process in case of exit (on both success and failures), +without deadlocking. +""" + +from __future__ import annotations + +import asyncio +import logging +import os + +# if TYPE_CHECKING: +from collections.abc import AsyncGenerator +from contextlib import suppress +from signal import SIGINT +from types import TracebackType +from typing import Self + +from music_assistant.constants import MASS_LOGGER_NAME, VERBOSE_LOG_LEVEL + +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.helpers.process") + +DEFAULT_CHUNKSIZE = 64000 + + +class AsyncProcess: + """ + AsyncProcess. + + Wrapper around asyncio subprocess to help with using pipe streams and + taking care of properly closing the process in case of exit (on both success and failures), + without deadlocking. + """ + + def __init__( + self, + args: list[str], + stdin: bool | int | None = None, + stdout: bool | int | None = None, + stderr: bool | int | None = False, + name: str | None = None, + ) -> None: + """Initialize AsyncProcess.""" + self.proc: asyncio.subprocess.Process | None = None + if name is None: + name = args[0].split(os.sep)[-1] + self.name = name + self.logger = LOGGER.getChild(name) + self._args = args + self._stdin = None if stdin is False else stdin + self._stdout = None if stdout is False else stdout + self._stderr = asyncio.subprocess.DEVNULL if stderr is False else stderr + self._close_called = False + self._returncode: bool | None = None + + @property + def closed(self) -> bool: + """Return if the process was closed.""" + return self._close_called or self.returncode is not None + + @property + def returncode(self) -> int | None: + """Return the erturncode of the process.""" + if self._returncode is not None: + return self._returncode + if self.proc is None: + return None + if (ret_code := self.proc.returncode) is not None: + self._returncode = ret_code + return ret_code + + async def __aenter__(self) -> Self: + """Enter context manager.""" + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit context manager.""" + # send interrupt signal to process when we're cancelled + await self.close(send_signal=exc_type in (GeneratorExit, asyncio.CancelledError)) + self._returncode = self.returncode + + async def start(self) -> None: + """Perform Async init of process.""" + for attempt in range(2): + try: + self.proc = await asyncio.create_subprocess_exec( + *self._args, + stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, + stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, + stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, + # because we're exchanging big amounts of (audio) data with pipes + # it makes sense to extend the pipe size and (buffer) limits a bit + limit=1000000 if attempt == 0 else 65536, + pipesize=1000000 if attempt == 0 else -1, + ) + break + except PermissionError: + if attempt > 0: + raise + LOGGER.error( + "Detected that you are running the (docker) container without " + "permissive access rights. This will impact performance !" + ) + + self.logger.log( + VERBOSE_LOG_LEVEL, "Process %s started with PID %s", self.name, self.proc.pid + ) + + async def iter_chunked(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]: + """Yield chunks of n size from the process stdout.""" + while True: + chunk = await self.readexactly(n) + if len(chunk) == 0: + break + yield chunk + + async def iter_any(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]: + """Yield chunks as they come in from process stdout.""" + while True: + chunk = await self.read(n) + if len(chunk) == 0: + break + yield chunk + + async def readexactly(self, n: int) -> bytes: + """Read exactly n bytes from the process stdout (or less if eof).""" + if self._close_called: + return b"" + try: + return await self.proc.stdout.readexactly(n) + except asyncio.IncompleteReadError as err: + return err.partial + + async def read(self, n: int) -> bytes: + """Read up to n bytes from the stdout stream. + + If n is positive, this function try to read n bytes, + and may return less or equal bytes than requested, but at least one byte. + If EOF was received before any byte is read, this function returns empty byte object. + """ + if self._close_called: + return b"" + return await self.proc.stdout.read(n) + + async def write(self, data: bytes) -> None: + """Write data to process stdin.""" + if self.closed: + self.logger.warning("write called while process already done") + return + self.proc.stdin.write(data) + with suppress(BrokenPipeError, ConnectionResetError): + await self.proc.stdin.drain() + + async def write_eof(self) -> None: + """Write end of file to to process stdin.""" + if self.closed: + return + try: + if self.proc.stdin.can_write_eof(): + self.proc.stdin.write_eof() + except ( + AttributeError, + AssertionError, + BrokenPipeError, + RuntimeError, + ConnectionResetError, + ): + # already exited, race condition + pass + + async def read_stderr(self) -> bytes: + """Read line from stderr.""" + if self._close_called: + return b"" + try: + return await self.proc.stderr.readline() + except ValueError as err: + # we're waiting for a line (separator found), but the line was too big + # this may happen with ffmpeg during a long (radio) stream where progress + # gets outputted to the stderr but no newline + # https://stackoverflow.com/questions/55457370/how-to-avoid-valueerror-separator-is-not-found-and-chunk-exceed-the-limit + # NOTE: this consumes the line that was too big + if "chunk exceed the limit" in str(err): + return await self.proc.stderr.readline() + # raise for all other (value) errors + raise + + async def iter_stderr(self) -> AsyncGenerator[str, None]: + """Iterate lines from the stderr stream as string.""" + while True: + line = await self.read_stderr() + if line == b"": + break + line = line.decode("utf-8", errors="ignore").strip() + if not line: + continue + yield line + + async def close(self, send_signal: bool = False) -> None: + """Close/terminate the process and wait for exit.""" + self._close_called = True + if send_signal and self.returncode is None: + self.proc.send_signal(SIGINT) + if self.proc.stdin and not self.proc.stdin.is_closing(): + self.proc.stdin.close() + # abort existing readers on stderr/stdout first before we send communicate + waiter: asyncio.Future + if self.proc.stdout and (waiter := self.proc.stdout._waiter): + self.proc.stdout._waiter = None + if waiter and not waiter.done(): + waiter.set_exception(asyncio.CancelledError()) + if self.proc.stderr and (waiter := self.proc.stderr._waiter): + self.proc.stderr._waiter = None + if waiter and not waiter.done(): + waiter.set_exception(asyncio.CancelledError()) + await asyncio.sleep(0) # yield to loop + + # make sure the process is really cleaned up. + # especially with pipes this can cause deadlocks if not properly guarded + # we need to ensure stdout and stderr are flushed and stdin closed + while self.returncode is None: + try: + # use communicate to flush all pipe buffers + await asyncio.wait_for(self.proc.communicate(), 5) + except RuntimeError as err: + if "read() called while another coroutine" in str(err): + # race condition + continue + raise + except TimeoutError: + self.logger.debug( + "Process %s with PID %s did not stop in time. Sending terminate...", + self.name, + self.proc.pid, + ) + self.proc.terminate() + self.logger.log( + VERBOSE_LOG_LEVEL, + "Process %s with PID %s stopped with returncode %s", + self.name, + self.proc.pid, + self.returncode, + ) + + async def wait(self) -> int: + """Wait for the process and return the returncode.""" + if self._returncode is None: + self._returncode = await self.proc.wait() + return self._returncode + + +async def check_output(*args: str, env: dict[str, str] | None = None) -> tuple[int, bytes]: + """Run subprocess and return returncode and output.""" + proc = await asyncio.create_subprocess_exec( + *args, stderr=asyncio.subprocess.STDOUT, stdout=asyncio.subprocess.PIPE, env=env + ) + stdout, _ = await proc.communicate() + return (proc.returncode, stdout) + + +async def communicate( + args: list[str], + input: bytes | None = None, # noqa: A002 +) -> tuple[int, bytes, bytes]: + """Communicate with subprocess and return returncode, stdout and stderr output.""" + proc = await asyncio.create_subprocess_exec( + *args, + stderr=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE if input is not None else None, + ) + stdout, stderr = await proc.communicate(input) + return (proc.returncode, stdout, stderr) diff --git a/music_assistant/helpers/resources/announce.mp3 b/music_assistant/helpers/resources/announce.mp3 new file mode 100644 index 00000000..6e2fa0ab Binary files /dev/null and b/music_assistant/helpers/resources/announce.mp3 differ diff --git a/music_assistant/helpers/resources/fallback_fanart.jpeg b/music_assistant/helpers/resources/fallback_fanart.jpeg new file mode 100644 index 00000000..24528fbe Binary files /dev/null and b/music_assistant/helpers/resources/fallback_fanart.jpeg differ diff --git a/music_assistant/helpers/resources/logo.png b/music_assistant/helpers/resources/logo.png new file mode 100644 index 00000000..d00d8ffd Binary files /dev/null and b/music_assistant/helpers/resources/logo.png differ diff --git a/music_assistant/helpers/resources/silence.mp3 b/music_assistant/helpers/resources/silence.mp3 new file mode 100644 index 00000000..38febc1b Binary files /dev/null and b/music_assistant/helpers/resources/silence.mp3 differ diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py new file mode 100644 index 00000000..818c2d31 --- /dev/null +++ b/music_assistant/helpers/tags.py @@ -0,0 +1,481 @@ +"""Helpers/utilities to parse ID3 tags from audio files with ffmpeg.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from collections.abc import Iterable +from dataclasses import dataclass +from json import JSONDecodeError +from typing import Any + +import eyed3 +from music_assistant_models.enums import AlbumType +from music_assistant_models.errors import InvalidDataError +from music_assistant_models.media_items import MediaItemChapter + +from music_assistant.constants import MASS_LOGGER_NAME, UNKNOWN_ARTIST +from music_assistant.helpers.process import AsyncProcess +from music_assistant.helpers.util import try_parse_int + +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.tags") + +# silence the eyed3 logger because it is too verbose +logging.getLogger("eyed3").setLevel(logging.ERROR) + + +# the only multi-item splitter we accept is the semicolon, +# which is also the default in Musicbrainz Picard. +# the slash is also a common splitter but causes collisions with +# artists actually containing a slash in the name, such as AC/DC +TAG_SPLITTER = ";" + + +def clean_tuple(values: Iterable[str]) -> tuple: + """Return a tuple with all empty values removed.""" + return tuple(x.strip() for x in values if x not in (None, "", " ")) + + +def split_items(org_str: str, allow_unsafe_splitters: bool = False) -> tuple[str, ...]: + """Split up a tags string by common splitter.""" + if org_str is None: + return () + if isinstance(org_str, list): + return (x.strip() for x in org_str) + org_str = org_str.strip() + if TAG_SPLITTER in org_str: + return clean_tuple(org_str.split(TAG_SPLITTER)) + if allow_unsafe_splitters and "/" in org_str: + return clean_tuple(org_str.split("/")) + if allow_unsafe_splitters and ", " in org_str: + return clean_tuple(org_str.split(", ")) + return clean_tuple((org_str,)) + + +def split_artists( + org_artists: str | tuple[str, ...], allow_ampersand: bool = False +) -> tuple[str, ...]: + """Parse all artists from a string.""" + final_artists = set() + # when not using the multi artist tag, the artist string may contain + # multiple artists in freeform, even featuring artists may be included in this + # string. Try to parse the featuring artists and separate them. + splitters = ("featuring", " feat. ", " feat ", "feat.") + if allow_ampersand: + splitters = (*splitters, " & ") + artists = split_items(org_artists) + for item in artists: + for splitter in splitters: + if splitter not in item: + continue + for subitem in item.split(splitter): + final_artists.add(subitem.strip()) + if not final_artists: + # none of the extra splitters was found + return artists + return tuple(final_artists) + + +@dataclass +class AudioTags: + """Audio metadata parsed from an audio file.""" + + raw: dict[str, Any] + sample_rate: int + channels: int + bits_per_sample: int + format: str + bit_rate: int | None + duration: float | None + tags: dict[str, str] + has_cover_image: bool + filename: str + + @property + def title(self) -> str: + """Return title tag (as-is).""" + if tag := self.tags.get("title"): + return tag + # fallback to parsing from filename + title = self.filename.rsplit(os.sep, 1)[-1].split(".")[0] + if " - " in title: + title_parts = title.split(" - ") + if len(title_parts) >= 2: + return title_parts[1].strip() + return title + + @property + def version(self) -> str: + """Return version tag (as-is).""" + if tag := self.tags.get("version"): + return tag + album_type_tag = ( + self.tags.get("musicbrainzalbumtype") + or self.tags.get("albumtype") + or self.tags.get("releasetype") + ) + if album_type_tag and "live" in album_type_tag.lower(): + # yes, this can happen + return "Live" + return "" + + @property + def album(self) -> str | None: + """Return album tag (as-is) if present.""" + return self.tags.get("album") + + @property + def artists(self) -> tuple[str, ...]: + """Return track artists.""" + # prefer multi-artist tag + if tag := self.tags.get("artists"): + return split_items(tag) + # fallback to regular artist string + if tag := self.tags.get("artist"): + if TAG_SPLITTER in tag: + return split_items(tag) + return split_artists(tag) + # fallback to parsing from filename + title = self.filename.rsplit(os.sep, 1)[-1].split(".")[0] + if " - " in title: + title_parts = title.split(" - ") + if len(title_parts) >= 2: + return split_artists(title_parts[0]) + return (UNKNOWN_ARTIST,) + + @property + def album_artists(self) -> tuple[str, ...]: + """Return (all) album artists (if any).""" + # prefer multi-artist tag + if tag := self.tags.get("albumartists"): + return split_items(tag) + # fallback to regular artist string + if tag := self.tags.get("albumartist"): + if TAG_SPLITTER in tag: + return split_items(tag) + if len(self.musicbrainz_albumartistids) > 1: + # special case: album artist noted as 2 artists with ampersand + # but with 2 mb ids so they should be treated as 2 artists + # example: John Travolta & Olivia Newton John on the Grease album + return split_artists(tag, allow_ampersand=True) + return split_artists(tag) + return () + + @property + def genres(self) -> tuple[str, ...]: + """Return (all) genres, if any.""" + return split_items(self.tags.get("genre")) + + @property + def disc(self) -> int | None: + """Return disc tag if present.""" + if tag := self.tags.get("disc"): + return try_parse_int(tag.split("/")[0], None) + return None + + @property + def track(self) -> int | None: + """Return track tag if present.""" + if tag := self.tags.get("track"): + return try_parse_int(tag.split("/")[0], None) + # fallback to parsing from filename (if present) + # this can be in the form of 01 - title.mp3 + # or 01-title.mp3 + # or 01.title.mp3 + # or 01 title.mp3 + # or 1. title.mp3 + for splitpos in (4, 3, 2, 1): + firstpart = self.filename[:splitpos] + if firstpart.isnumeric(): + return try_parse_int(firstpart, None) + return None + + @property + def year(self) -> int | None: + """Return album's year if present, parsed from date.""" + if tag := self.tags.get("originalyear"): + return try_parse_int(tag.split("-")[0], None) + if tag := self.tags.get("originaldate"): + return try_parse_int(tag.split("-")[0], None) + if tag := self.tags.get("date"): + return try_parse_int(tag.split("-")[0], None) + return None + + @property + def musicbrainz_artistids(self) -> tuple[str, ...]: + """Return musicbrainz_artistid tag(s) if present.""" + return split_items(self.tags.get("musicbrainzartistid"), True) + + @property + def musicbrainz_albumartistids(self) -> tuple[str, ...]: + """Return musicbrainz_albumartistid tag if present.""" + if tag := self.tags.get("musicbrainzalbumartistid"): + return split_items(tag, True) + return split_items(self.tags.get("musicbrainzreleaseartistid"), True) + + @property + def musicbrainz_releasegroupid(self) -> str | None: + """Return musicbrainz_releasegroupid tag if present.""" + return self.tags.get("musicbrainzreleasegroupid") + + @property + def musicbrainz_albumid(self) -> str | None: + """Return musicbrainz_albumid tag if present.""" + return self.tags.get("musicbrainzreleaseid", self.tags.get("musicbrainzalbumid")) + + @property + def musicbrainz_recordingid(self) -> str | None: + """Return musicbrainz_recordingid tag if present.""" + if tag := self.tags.get("UFID:http://musicbrainz.org"): + return tag + if tag := self.tags.get("musicbrainz.org"): + return tag + if tag := self.tags.get("musicbrainzrecordingid"): + return tag + return self.tags.get("musicbrainztrackid") + + @property + def title_sort(self) -> str | None: + """Return sort title tag (if exists).""" + if tag := self.tags.get("titlesort"): + return tag + return None + + @property + def album_sort(self) -> str | None: + """Return album sort title tag (if exists).""" + if tag := self.tags.get("albumsort"): + return tag + return None + + @property + def artist_sort_names(self) -> tuple[str, ...]: + """Return artist sort name tag(s) if present.""" + return split_items(self.tags.get("artistsort"), False) + + @property + def album_artist_sort_names(self) -> tuple[str, ...]: + """Return artist sort name tag(s) if present.""" + return split_items(self.tags.get("albumartistsort"), False) + + @property + def album_type(self) -> AlbumType: + """Return albumtype tag if present.""" + # handle audiobook/podcast + if self.filename.endswith("m4b") and len(self.chapters) > 1: + return AlbumType.AUDIOBOOK + if "podcast" in self.tags.get("genre", "").lower() and len(self.chapters) > 1: + return AlbumType.PODCAST + if self.tags.get("compilation", "") == "1": + return AlbumType.COMPILATION + tag = ( + self.tags.get("musicbrainzalbumtype") + or self.tags.get("albumtype") + or self.tags.get("releasetype") + ) + if tag is None: + return AlbumType.UNKNOWN + # the album type tag is messy within id3 and may even contain multiple types + # try to parse one in order of preference + for album_type in ( + AlbumType.PODCAST, + AlbumType.AUDIOBOOK, + AlbumType.COMPILATION, + AlbumType.EP, + AlbumType.SINGLE, + AlbumType.ALBUM, + ): + if album_type.value in tag.lower(): + return album_type + + return AlbumType.UNKNOWN + + @property + def isrc(self) -> tuple[str]: + """Return isrc tag(s).""" + for tag_name in ("isrc", "tsrc"): + if tag := self.tags.get(tag_name): + # sometimes the field contains multiple values + return split_items(tag, True) + return () + + @property + def barcode(self) -> str | None: + """Return barcode (upc/ean) tag(s).""" + for tag_name in ("barcode", "upc", "ean"): + if tag := self.tags.get(tag_name): + # sometimes the field contains multiple values + # we only need one + for item in split_items(tag, True): + if len(item) == 12: + # convert UPC barcode to EAN-13 + return f"0{item}" + return item + return None + + @property + def chapters(self) -> list[MediaItemChapter]: + """Return chapters in MediaItem (if any).""" + chapters: list[MediaItemChapter] = [] + if raw_chapters := self.raw.get("chapters"): + for chapter_data in raw_chapters: + chapters.append( + MediaItemChapter( + chapter_id=chapter_data["id"], + position_start=chapter_data["start"], + position_end=chapter_data["end"], + title=chapter_data.get("tags", {}).get("title"), + ) + ) + return chapters + + @property + def lyrics(self) -> str | None: + """Return lyrics tag (if exists).""" + for key, value in self.tags.items(): + if key.startswith("lyrics"): + return value + return None + + @property + def track_loudness(self) -> float | None: + """Try to read/calculate the integrated loudness from the tags.""" + if (tag := self.tags.get("r128trackgain")) is not None: + return -23 - float(int(tag.split(" ")[0]) / 256) + if (tag := self.tags.get("replaygaintrackgain")) is not None: + return -18 - float(tag.split(" ")[0]) + return None + + @property + def track_album_loudness(self) -> float | None: + """Try to read/calculate the integrated loudness from the tags (album level).""" + if tag := self.tags.get("r128albumgain"): + return -23 - float(int(tag.split(" ")[0]) / 256) + if (tag := self.tags.get("replaygainalbumgain")) is not None: + return -18 - float(tag.split(" ")[0]) + return None + + @classmethod + def parse(cls, raw: dict) -> AudioTags: + """Parse instance from raw ffmpeg info output.""" + audio_stream = next((x for x in raw["streams"] if x["codec_type"] == "audio"), None) + if audio_stream is None: + msg = "No audio stream found" + raise InvalidDataError(msg) + has_cover_image = any( + x for x in raw["streams"] if x.get("codec_name", "") in ("mjpeg", "png") + ) + # convert all tag-keys (gathered from all streams) to lowercase without spaces + tags = {} + for stream in raw["streams"] + [raw["format"]]: + for key, value in stream.get("tags", {}).items(): + alt_key = key.lower().replace(" ", "").replace("_", "").replace("-", "") + tags[alt_key] = value + + return AudioTags( + raw=raw, + sample_rate=int(audio_stream.get("sample_rate", 44100)), + channels=audio_stream.get("channels", 2), + bits_per_sample=int( + audio_stream.get("bits_per_raw_sample", audio_stream.get("bits_per_sample")) or 16 + ), + format=raw["format"]["format_name"], + bit_rate=int(raw["format"].get("bit_rate", 0)) or None, + duration=float(raw["format"].get("duration", 0)) or None, + tags=tags, + has_cover_image=has_cover_image, + filename=raw["format"]["filename"], + ) + + def get(self, key: str, default=None) -> Any: + """Get tag by key.""" + return self.tags.get(key, default) + + +async def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags: + """ + Parse tags from a media file (or URL). + + Input_file may be a (local) filename or URL accessible by ffmpeg. + """ + args = ( + "ffprobe", + "-hide_banner", + "-loglevel", + "fatal", + "-threads", + "0", + "-show_error", + "-show_format", + "-show_streams", + "-show_chapters", + "-print_format", + "json", + "-i", + input_file, + ) + async with AsyncProcess(args, stdin=False, stdout=True) as ffmpeg: + res = await ffmpeg.read(-1) + try: + data = json.loads(res) + if error := data.get("error"): + raise InvalidDataError(error["string"]) + if not data.get("streams"): + msg = "Not an audio file" + raise InvalidDataError(msg) + tags = AudioTags.parse(data) + del res + del data + if not tags.duration and file_size and tags.bit_rate: + # estimate duration from filesize/bitrate + tags.duration = int((file_size * 8) / tags.bit_rate) + if not tags.duration and tags.raw.get("format", {}).get("duration"): + tags.duration = float(tags.raw["format"]["duration"]) + + if ( + not input_file.startswith("http") + and input_file.endswith(".mp3") + and "musicbrainzrecordingid" not in tags.tags + and await asyncio.to_thread(os.path.isfile, input_file) + ): + # eyed3 is able to extract the musicbrainzrecordingid from the unique file id + # this is actually a bug in ffmpeg/ffprobe which does not expose this tag + # so we use this as alternative approach for mp3 files + audiofile = await asyncio.to_thread(eyed3.load, input_file) + if audiofile is not None and audiofile.tag is not None: + for uf_id in audiofile.tag.unique_file_ids: + if uf_id.owner_id == b"http://musicbrainz.org" and uf_id.uniq_id: + tags.tags["musicbrainzrecordingid"] = uf_id.uniq_id.decode() + break + del audiofile + return tags + except (KeyError, ValueError, JSONDecodeError, InvalidDataError) as err: + msg = f"Unable to retrieve info for {input_file}: {err!s}" + raise InvalidDataError(msg) from err + + +async def get_embedded_image(input_file: str) -> bytes | None: + """Return embedded image data. + + Input_file may be a (local) filename or URL accessible by ffmpeg. + """ + args = ( + "ffmpeg", + "-hide_banner", + "-loglevel", + "error", + "-i", + input_file, + "-an", + "-vcodec", + "mjpeg", + "-f", + "mjpeg", + "-", + ) + async with AsyncProcess( + args, stdin=False, stdout=True, stderr=None, name="ffmpeg_image" + ) as ffmpeg: + return await ffmpeg.read(-1) diff --git a/music_assistant/helpers/throttle_retry.py b/music_assistant/helpers/throttle_retry.py new file mode 100644 index 00000000..74a95738 --- /dev/null +++ b/music_assistant/helpers/throttle_retry.py @@ -0,0 +1,133 @@ +"""Context manager using asyncio_throttle that catches and re-raises RetriesExhausted.""" + +import asyncio +import functools +import logging +import time +from collections import deque +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine +from contextlib import asynccontextmanager +from contextvars import ContextVar +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar + +from music_assistant_models.errors import ResourceTemporarilyUnavailable, RetriesExhausted + +from music_assistant.constants import MASS_LOGGER_NAME + +if TYPE_CHECKING: + from music_assistant.models.provider import Provider + +_ProviderT = TypeVar("_ProviderT", bound="Provider") +_R = TypeVar("_R") +_P = ParamSpec("_P") +LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.throttle_retry") + +BYPASS_THROTTLER: ContextVar[bool] = ContextVar("BYPASS_THROTTLER", default=False) + + +class Throttler: + """asyncio_throttle (https://github.com/hallazzang/asyncio-throttle). + + With improvements: + - Accurate sleep without "busy waiting" (PR #4) + - Return the delay caused by acquire() + """ + + def __init__(self, rate_limit: int, period=1.0): + """Initialize the Throttler.""" + self.rate_limit = rate_limit + self.period = period + self._task_logs: deque[float] = deque() + + def _flush(self): + now = time.monotonic() + while self._task_logs: + if now - self._task_logs[0] > self.period: + self._task_logs.popleft() + else: + break + + async def acquire(self) -> float: + """Acquire a free slot from the Throttler, returns the throttled time.""" + cur_time = time.monotonic() + start_time = cur_time + while True: + self._flush() + if len(self._task_logs) < self.rate_limit: + break + # sleep the exact amount of time until the oldest task can be flushed + time_to_release = self._task_logs[0] + self.period - cur_time + await asyncio.sleep(time_to_release) + cur_time = time.monotonic() + + self._task_logs.append(cur_time) + return cur_time - start_time # exactly 0 if not throttled + + async def __aenter__(self) -> float: + """Wait until the lock is acquired, return the time delay.""" + return await self.acquire() + + async def __aexit__(self, exc_type, exc, tb): + """Nothing to do on exit.""" + + +class ThrottlerManager: + """Throttler manager that extends asyncio Throttle by retrying.""" + + def __init__(self, rate_limit: int, period: float = 1, retry_attempts=5, initial_backoff=5): + """Initialize the AsyncThrottledContextManager.""" + self.retry_attempts = retry_attempts + self.initial_backoff = initial_backoff + self.throttler = Throttler(rate_limit, period) + + @asynccontextmanager + async def acquire(self) -> AsyncGenerator[None, float]: + """Acquire a free slot from the Throttler, returns the throttled time.""" + if BYPASS_THROTTLER.get(): + yield 0 + else: + yield await self.throttler.acquire() + + @asynccontextmanager + async def bypass(self) -> AsyncGenerator[None, None]: + """Bypass the throttler.""" + try: + token = BYPASS_THROTTLER.set(True) + yield None + finally: + BYPASS_THROTTLER.reset(token) + + +def throttle_with_retries( + func: Callable[Concatenate[_ProviderT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_ProviderT, _P], Coroutine[Any, Any, _R]]: + """Call async function using the throttler with retries.""" + + @functools.wraps(func) + async def wrapper(self: _ProviderT, *args: _P.args, **kwargs: _P.kwargs) -> _R: + """Call async function using the throttler with retries.""" + # the trottler attribute must be present on the class + throttler: ThrottlerManager = self.throttler + backoff_time = throttler.initial_backoff + async with throttler.acquire() as delay: + if delay != 0: + self.logger.debug( + "%s was delayed for %.3f secs due to throttling", func.__name__, delay + ) + for attempt in range(throttler.retry_attempts): + try: + return await func(self, *args, **kwargs) + except ResourceTemporarilyUnavailable as e: + backoff_time = e.backoff_time or backoff_time + self.logger.info( + f"Attempt {attempt + 1}/{throttler.retry_attempts} failed: {e}" + ) + if attempt < throttler.retry_attempts - 1: + self.logger.info(f"Retrying in {backoff_time} seconds...") + await asyncio.sleep(backoff_time) + backoff_time *= 2 + else: # noqa: PLW0120 + msg = f"Retries exhausted, failed after {throttler.retry_attempts} attempts" + raise RetriesExhausted(msg) + + return wrapper diff --git a/music_assistant/helpers/uri.py b/music_assistant/helpers/uri.py new file mode 100644 index 00000000..7f39b4e1 --- /dev/null +++ b/music_assistant/helpers/uri.py @@ -0,0 +1,74 @@ +"""Helpers for creating/parsing URI's.""" + +import asyncio +import os +import re + +from music_assistant_models.enums import MediaType +from music_assistant_models.errors import InvalidProviderID, InvalidProviderURI + +base62_length22_id_pattern = re.compile(r"^[a-zA-Z0-9]{22}$") + + +def valid_base62_length22(item_id: str) -> bool: + """Validate Spotify style ID.""" + return bool(base62_length22_id_pattern.match(item_id)) + + +def valid_id(provider: str, item_id: str) -> bool: + """Validate Provider ID.""" + if provider == "spotify": + return valid_base62_length22(item_id) + else: + return True + + +async def parse_uri(uri: str, validate_id: bool = False) -> tuple[MediaType, str, str]: + """Try to parse URI to Mass identifiers. + + Returns Tuple: MediaType, provider_instance_id_or_domain, item_id + """ + try: + if uri.startswith("https://open."): + # public share URL (e.g. Spotify or Qobuz, not sure about others) + # https://open.spotify.com/playlist/5lH9NjOeJvctAO92ZrKQNB?si=04a63c8234ac413e + provider_instance_id_or_domain = uri.split(".")[1] + media_type_str = uri.split("/")[3] + media_type = MediaType(media_type_str) + item_id = uri.split("/")[4].split("?")[0] + elif uri.startswith("https://tidal.com/browse/"): + # Tidal public share URL + # https://tidal.com/browse/track/123456 + provider_instance_id_or_domain = "tidal" + media_type_str = uri.split("/")[4] + media_type = MediaType(media_type_str) + item_id = uri.split("/")[5].split("?")[0] + elif uri.startswith(("http://", "https://", "rtsp://", "rtmp://")): + # Translate a plain URL to the builtin provider + provider_instance_id_or_domain = "builtin" + media_type = MediaType.UNKNOWN + item_id = uri + elif "://" in uri and len(uri.split("/")) >= 4: + # music assistant-style uri + # provider://media_type/item_id + provider_instance_id_or_domain, rest = uri.split("://", 1) + media_type_str, item_id = rest.split("/", 1) + media_type = MediaType(media_type_str) + elif ":" in uri and len(uri.split(":")) == 3: + # spotify new-style uri + provider_instance_id_or_domain, media_type_str, item_id = uri.split(":") + media_type = MediaType(media_type_str) + elif "/" in uri and await asyncio.to_thread(os.path.isfile, uri): + # Translate a local file (which is not from a file provider!) to the builtin provider + provider_instance_id_or_domain = "builtin" + media_type = MediaType.UNKNOWN + item_id = uri + else: + raise KeyError + except (TypeError, AttributeError, ValueError, KeyError) as err: + msg = f"Not a valid Music Assistant uri: {uri}" + raise InvalidProviderURI(msg) from err + if validate_id and not valid_id(provider_instance_id_or_domain, item_id): + msg = f"Invalid {provider_instance_id_or_domain} ID: {item_id} found in URI: {uri}" + raise InvalidProviderID(msg) + return (media_type, provider_instance_id_or_domain, item_id) diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py new file mode 100644 index 00000000..25da351a --- /dev/null +++ b/music_assistant/helpers/util.py @@ -0,0 +1,633 @@ +"""Various (server-only) tools and helpers.""" + +from __future__ import annotations + +import asyncio +import functools +import importlib +import logging +import os +import platform +import re +import socket +import tempfile +import urllib.error +import urllib.parse +import urllib.request +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine +from collections.abc import Set as AbstractSet +from contextlib import suppress +from functools import lru_cache +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version +from types import TracebackType +from typing import TYPE_CHECKING, Any, ParamSpec, Self, TypeVar +from urllib.parse import urlparse + +import cchardet as chardet +import ifaddr +import memory_tempfile +from zeroconf import IPVersion + +from music_assistant.helpers.process import check_output + +if TYPE_CHECKING: + from collections.abc import Iterator + + from zeroconf.asyncio import AsyncServiceInfo + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderModuleType + +LOGGER = logging.getLogger(__name__) + +HA_WHEELS = "https://wheels.home-assistant.io/musllinux/" + +T = TypeVar("T") +CALLBACK_TYPE = Callable[[], None] + + +keyword_pattern = re.compile("title=|artist=") +title_pattern = re.compile(r"title=\"(?P<title>.*?)\"") +artist_pattern = re.compile(r"artist=\"(?P<artist>.*?)\"") +dot_com_pattern = re.compile(r"(?P<netloc>\(?\w+\.(?:\w+\.)?(\w{2,3})\)?)") +ad_pattern = re.compile(r"((ad|advertisement)_)|^AD\s\d+$|ADBREAK", flags=re.IGNORECASE) +title_artist_order_pattern = re.compile(r"(?P<title>.+)\sBy:\s(?P<artist>.+)", flags=re.IGNORECASE) +multi_space_pattern = re.compile(r"\s{2,}") +end_junk_pattern = re.compile(r"(.+?)(\s\W+)$") + +VERSION_PARTS = ( + # list of common version strings + "version", + "live", + "edit", + "remix", + "mix", + "acoustic", + "instrumental", + "karaoke", + "remaster", + "versie", + "unplugged", + "disco", + "akoestisch", + "deluxe", +) +IGNORE_TITLE_PARTS = ( + # strings that may be stripped off a title part + # (most important the featuring parts) + "feat.", + "featuring", + "ft.", + "with ", + "explicit", +) + + +def filename_from_string(string: str) -> str: + """Create filename from unsafe string.""" + keepcharacters = (" ", ".", "_") + return "".join(c for c in string if c.isalnum() or c in keepcharacters).rstrip() + + +def try_parse_int(possible_int: Any, default: int | None = 0) -> int | None: + """Try to parse an int.""" + try: + return int(possible_int) + except (TypeError, ValueError): + return default + + +def try_parse_float(possible_float: Any, default: float | None = 0.0) -> float | None: + """Try to parse a float.""" + try: + return float(possible_float) + except (TypeError, ValueError): + return default + + +def try_parse_bool(possible_bool: Any) -> bool: + """Try to parse a bool.""" + if isinstance(possible_bool, bool): + return possible_bool + return possible_bool in ["true", "True", "1", "on", "ON", 1] + + +def try_parse_duration(duration_str: str) -> float: + """Try to parse a duration in seconds from a duration (HH:MM:SS) string.""" + milliseconds = float("0." + duration_str.split(".")[-1]) if "." in duration_str else 0.0 + duration_parts = duration_str.split(".")[0].split(",")[0].split(":") + if len(duration_parts) == 3: + seconds = sum(x * int(t) for x, t in zip([3600, 60, 1], duration_parts, strict=False)) + elif len(duration_parts) == 2: + seconds = sum(x * int(t) for x, t in zip([60, 1], duration_parts, strict=False)) + else: + seconds = int(duration_parts[0]) + return seconds + milliseconds + + +def parse_title_and_version(title: str, track_version: str | None = None) -> tuple[str, str]: + """Try to parse version from the title.""" + version = track_version or "" + for regex in (r"\(.*?\)", r"\[.*?\]", r" - .*"): + for title_part in re.findall(regex, title): + for ignore_str in IGNORE_TITLE_PARTS: + if ignore_str in title_part.lower(): + title = title.replace(title_part, "").strip() + continue + for version_str in VERSION_PARTS: + if version_str not in title_part.lower(): + continue + version = ( + title_part.replace("(", "") + .replace(")", "") + .replace("[", "") + .replace("]", "") + .replace("-", "") + .strip() + ) + title = title.replace(title_part, "").strip() + return (title, version) + return title, version + + +def strip_ads(line: str) -> str: + """Strip Ads from line.""" + if ad_pattern.search(line): + return "Advert" + return line + + +def strip_url(line: str) -> str: + """Strip URL from line.""" + return ( + " ".join([p for p in line.split() if (not urlparse(p).scheme or not urlparse(p).netloc)]) + ).rstrip() + + +def strip_dotcom(line: str) -> str: + """Strip scheme-less netloc from line.""" + return dot_com_pattern.sub("", line) + + +def strip_end_junk(line: str) -> str: + """Strip non-word info from end of line.""" + return end_junk_pattern.sub(r"\1", line) + + +def swap_title_artist_order(line: str) -> str: + """Swap title/artist order in line.""" + return title_artist_order_pattern.sub(r"\g<artist> - \g<title>", line) + + +def strip_multi_space(line: str) -> str: + """Strip multi-whitespace from line.""" + return multi_space_pattern.sub(" ", line) + + +def multi_strip(line: str) -> str: + """Strip assorted junk from line.""" + return strip_multi_space( + swap_title_artist_order(strip_end_junk(strip_dotcom(strip_url(strip_ads(line))))) + ).rstrip() + + +def clean_stream_title(line: str) -> str: + """Strip junk text from radio streamtitle.""" + title: str = "" + artist: str = "" + + if not keyword_pattern.search(line): + return multi_strip(line) + + if match := title_pattern.search(line): + title = multi_strip(match.group("title")) + + if match := artist_pattern.search(line): + possible_artist = multi_strip(match.group("artist")) + if possible_artist and possible_artist != title: + artist = possible_artist + + if not title and not artist: + return "" + + if title: + if re.search(" - ", title) or not artist: + return title + if artist: + return f"{artist} - {title}" + + if artist: + return artist + + return line + + +async def get_ip() -> str: + """Get primary IP-address for this host.""" + + def _get_ip() -> str: + """Get primary IP-address for this host.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + sock.connect(("10.255.255.255", 1)) + _ip = str(sock.getsockname()[0]) + except Exception: + _ip = "127.0.0.1" + finally: + sock.close() + return _ip + + return await asyncio.to_thread(_get_ip) + + +async def is_port_in_use(port: int) -> bool: + """Check if port is in use.""" + + def _is_port_in_use() -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as _sock: + try: + _sock.bind(("0.0.0.0", port)) + except OSError: + return True + return False + + return await asyncio.to_thread(_is_port_in_use) + + +async def select_free_port(range_start: int, range_end: int) -> int: + """Automatically find available port within range.""" + for port in range(range_start, range_end): + if not await is_port_in_use(port): + return port + msg = "No free port available" + raise OSError(msg) + + +async def get_ip_from_host(dns_name: str) -> str | None: + """Resolve (first) IP-address for given dns name.""" + + def _resolve() -> str | None: + try: + return socket.gethostbyname(dns_name) + except Exception: + # fail gracefully! + return None + + return await asyncio.to_thread(_resolve) + + +async def get_ip_pton(ip_string: str | None = None) -> bytes: + """Return socket pton for local ip.""" + if ip_string is None: + ip_string = await get_ip() + try: + return await asyncio.to_thread(socket.inet_pton, socket.AF_INET, ip_string) + except OSError: + return await asyncio.to_thread(socket.inet_pton, socket.AF_INET6, ip_string) + + +def get_folder_size(folderpath: str) -> float: + """Return folder size in gb.""" + total_size = 0 + for dirpath, _dirnames, filenames in os.walk(folderpath): + for _file in filenames: + _fp = os.path.join(dirpath, _file) + total_size += os.path.getsize(_fp) + return total_size / float(1 << 30) + + +def get_changed_keys( + dict1: dict[str, Any], + dict2: dict[str, Any], + ignore_keys: list[str] | None = None, +) -> AbstractSet[str]: + """Compare 2 dicts and return set of changed keys.""" + return get_changed_values(dict1, dict2, ignore_keys).keys() + + +def get_changed_values( + dict1: dict[str, Any], + dict2: dict[str, Any], + ignore_keys: list[str] | None = None, +) -> dict[str, tuple[Any, Any]]: + """ + Compare 2 dicts and return dict of changed values. + + dict key is the changed key, value is tuple of old and new values. + """ + if not dict1 and not dict2: + return {} + if not dict1: + return {key: (None, value) for key, value in dict2.items()} + if not dict2: + return {key: (None, value) for key, value in dict1.items()} + changed_values = {} + for key, value in dict2.items(): + if ignore_keys and key in ignore_keys: + continue + if key not in dict1: + changed_values[key] = (None, value) + elif isinstance(value, dict): + changed_values.update(get_changed_values(dict1[key], value, ignore_keys)) + elif dict1[key] != value: + changed_values[key] = (dict1[key], value) + return changed_values + + +def empty_queue(q: asyncio.Queue[T]) -> None: + """Empty an asyncio Queue.""" + for _ in range(q.qsize()): + try: + q.get_nowait() + q.task_done() + except (asyncio.QueueEmpty, ValueError): + pass + + +async def install_package(package: str) -> None: + """Install package with pip, raise when install failed.""" + LOGGER.debug("Installing python package %s", package) + args = ["uv", "pip", "install", "--no-cache", "--find-links", HA_WHEELS, package] + return_code, output = await check_output(*args) + + if return_code != 0 and "Permission denied" in output.decode(): + # try again with regular pip + # uv pip seems to have issues with permissions on docker installs + args = [ + "pip", + "install", + "--no-cache-dir", + "--no-input", + "--find-links", + HA_WHEELS, + package, + ] + return_code, output = await check_output(*args) + + if return_code != 0: + msg = f"Failed to install package {package}\n{output.decode()}" + raise RuntimeError(msg) + + +async def get_package_version(pkg_name: str) -> str | None: + """ + Return the version of an installed (python) package. + + Will return None if the package is not found. + """ + try: + return await asyncio.to_thread(pkg_version, pkg_name) + except PackageNotFoundError: + return None + + +async def get_ips(include_ipv6: bool = False, ignore_loopback: bool = True) -> set[str]: + """Return all IP-adresses of all network interfaces.""" + + def call() -> set[str]: + result: set[str] = set() + adapters = ifaddr.get_adapters() + for adapter in adapters: + for ip in adapter.ips: + if ip.is_IPv6 and not include_ipv6: + continue + if ip.ip == "127.0.0.1" and ignore_loopback: + continue + result.add(ip.ip) + return result + + return await asyncio.to_thread(call) + + +async def is_hass_supervisor() -> bool: + """Return if we're running inside the HA Supervisor (e.g. HAOS).""" + + def _check(): + try: + urllib.request.urlopen("http://supervisor/core", timeout=1) + except urllib.error.URLError as err: + # this should return a 401 unauthorized if it exists + return getattr(err, "code", 999) == 401 + except Exception: + return False + return False + + return await asyncio.to_thread(_check) + + +async def load_provider_module(domain: str, requirements: list[str]) -> ProviderModuleType: + """Return module for given provider domain and make sure the requirements are met.""" + + @lru_cache + def _get_provider_module(domain: str) -> ProviderModuleType: + return importlib.import_module(f".{domain}", ".providers") + + # ensure module requirements are met + for requirement in requirements: + if "==" not in requirement: + # we should really get rid of unpinned requirements + continue + package_name, version = requirement.split("==", 1) + installed_version = await get_package_version(package_name) + if installed_version == "0.0.0": + # ignore editable installs + continue + if installed_version != version: + await install_package(requirement) + + # try to load the module + try: + return await asyncio.to_thread(_get_provider_module, domain) + except ImportError: + # (re)install ALL requirements + for requirement in requirements: + await install_package(requirement) + # try loading the provider again to be safe + # this will fail if something else is wrong (as it should) + return await asyncio.to_thread(_get_provider_module, domain) + + +def create_tempfile(): + """Return a (named) temporary file.""" + # ruff: noqa: SIM115 + if platform.system() == "Linux": + return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) + return tempfile.NamedTemporaryFile(buffering=0) + + +def divide_chunks(data: bytes, chunk_size: int) -> Iterator[bytes]: + """Chunk bytes data into smaller chunks.""" + for i in range(0, len(data), chunk_size): + yield data[i : i + chunk_size] + + +def get_primary_ip_address_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + for address in discovery_info.parsed_addresses(IPVersion.V4Only): + if address.startswith("127"): + # filter out loopback address + continue + if address.startswith("169.254"): + # filter out APIPA address + continue + return address + return None + + +def get_port_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + return discovery_info.port + + +async def close_async_generator(agen: AsyncGenerator[Any, None]) -> None: + """Force close an async generator.""" + task = asyncio.create_task(agen.__anext__()) + task.cancel() + with suppress(asyncio.CancelledError): + await task + await agen.aclose() + + +async def detect_charset(data: bytes, fallback="utf-8") -> str: + """Detect charset of raw data.""" + try: + detected = await asyncio.to_thread(chardet.detect, data) + if detected and detected["encoding"] and detected["confidence"] > 0.75: + return detected["encoding"] + except Exception as err: + LOGGER.debug("Failed to detect charset: %s", err) + return fallback + + +def merge_dict( + base_dict: dict[Any, Any], + new_dict: dict[Any, Any], + allow_overwite: bool = False, +) -> dict[Any, Any]: + """Merge dict without overwriting existing values.""" + final_dict = base_dict.copy() + for key, value in new_dict.items(): + if final_dict.get(key) and isinstance(value, dict): + final_dict[key] = merge_dict(final_dict[key], value) + if final_dict.get(key) and isinstance(value, tuple): + final_dict[key] = merge_tuples(final_dict[key], value) + if final_dict.get(key) and isinstance(value, list): + final_dict[key] = merge_lists(final_dict[key], value) + elif not final_dict.get(key) or allow_overwite: + final_dict[key] = value + return final_dict + + +def merge_tuples(base: tuple[Any, ...], new: tuple[Any, ...]) -> tuple[Any, ...]: + """Merge 2 tuples.""" + return tuple(x for x in base if x not in new) + tuple(new) + + +def merge_lists(base: list[Any], new: list[Any]) -> list[Any]: + """Merge 2 lists.""" + return [x for x in base if x not in new] + list(new) + + +class TaskManager: + """ + Helper class to run many tasks at once. + + This is basically an alternative to asyncio.TaskGroup but this will not + cancel all operations when one of the tasks fails. + Logging of exceptions is done by the mass.create_task helper. + """ + + def __init__(self, mass: MusicAssistant, limit: int = 0): + """Initialize the TaskManager.""" + self.mass = mass + self._tasks: list[asyncio.Task] = [] + self._semaphore = asyncio.Semaphore(limit) if limit else None + + def create_task(self, coro: Coroutine) -> asyncio.Task: + """Create a new task and add it to the manager.""" + task = self.mass.create_task(coro) + self._tasks.append(task) + return task + + async def create_task_with_limit(self, coro: Coroutine) -> None: + """Create a new task with semaphore limit.""" + assert self._semaphore is not None + + def task_done_callback(_task: asyncio.Task) -> None: + self._tasks.remove(task) + self._semaphore.release() + + await self._semaphore.acquire() + task: asyncio.Task = self.create_task(coro) + task.add_done_callback(task_done_callback) + + async def __aenter__(self) -> Self: + """Enter context manager.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit context manager.""" + if len(self._tasks) > 0: + await asyncio.wait(self._tasks) + self._tasks.clear() + + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def lock( + func: Callable[_P, Awaitable[_R]], +) -> Callable[_P, Coroutine[Any, Any, _R]]: + """Call async function using a Lock.""" + + @functools.wraps(func) + async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + """Call async function using the throttler with retries.""" + if not (func_lock := getattr(func, "lock", None)): + func_lock = asyncio.Lock() + func.lock = func_lock + async with func_lock: + return await func(*args, **kwargs) + + return wrapper + + +class TimedAsyncGenerator: + """ + Async iterable that times out after a given time. + + Source: https://medium.com/@dmitry8912/implementing-timeouts-in-pythons-asynchronous-generators-f7cbaa6dc1e9 + """ + + def __init__(self, iterable, timeout=0): + """ + Initialize the AsyncTimedIterable. + + Args: + iterable: The async iterable to wrap. + timeout: The timeout in seconds for each iteration. + """ + + class AsyncTimedIterator: + def __init__(self): + self._iterator = iterable.__aiter__() + + async def __anext__(self): + result = await asyncio.wait_for(self._iterator.__anext__(), int(timeout)) + if not result: + raise StopAsyncIteration + return result + + self._factory = AsyncTimedIterator + + def __aiter__(self): + """Return the async iterator.""" + return self._factory() diff --git a/music_assistant/helpers/webserver.py b/music_assistant/helpers/webserver.py new file mode 100644 index 00000000..249df4fe --- /dev/null +++ b/music_assistant/helpers/webserver.py @@ -0,0 +1,145 @@ +"""Base Webserver logic for an HTTPServer that can handle dynamic routes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from aiohttp import web + +if TYPE_CHECKING: + import logging + from collections.abc import Awaitable, Callable + +MAX_CLIENT_SIZE: Final = 1024**2 * 16 +MAX_LINE_SIZE: Final = 24570 + + +class Webserver: + """Base Webserver logic for an HTTPServer that can handle dynamic routes.""" + + def __init__( + self, + logger: logging.Logger, + enable_dynamic_routes: bool = False, + ) -> None: + """Initialize instance.""" + self.logger = logger + # the below gets initialized in async setup + self._apprunner: web.AppRunner | None = None + self._webapp: web.Application | None = None + self._tcp_site: web.TCPSite | None = None + self._static_routes: list[tuple[str, str, Awaitable]] | None = None + self._dynamic_routes: dict[str, Callable] | None = {} if enable_dynamic_routes else None + self._bind_port: int | None = None + + async def setup( + self, + bind_ip: str | None, + bind_port: int, + base_url: str, + static_routes: list[tuple[str, str, Awaitable]] | None = None, + static_content: tuple[str, str, str] | None = None, + ) -> None: + """Async initialize of module.""" + self._base_url = base_url[:-1] if base_url.endswith("/") else base_url + self._bind_port = bind_port + self._static_routes = static_routes + self._webapp = web.Application( + logger=self.logger, + client_max_size=MAX_CLIENT_SIZE, + handler_args={ + "max_line_size": MAX_LINE_SIZE, + "max_field_size": MAX_LINE_SIZE, + }, + ) + self.logger.info("Starting server on %s:%s - base url: %s", bind_ip, bind_port, base_url) + self._apprunner = web.AppRunner(self._webapp, access_log=None, shutdown_timeout=10) + # add static routes + if self._static_routes: + for method, path, handler in self._static_routes: + self._webapp.router.add_route(method, path, handler) + if static_content: + self._webapp.router.add_static( + static_content[0], static_content[1], name=static_content[2] + ) + # register catch-all route to handle dynamic routes (if enabled) + if self._dynamic_routes is not None: + self._webapp.router.add_route("*", "/{tail:.*}", self._handle_catch_all) + await self._apprunner.setup() + # set host to None to bind to all addresses on both IPv4 and IPv6 + host = None if bind_ip == "0.0.0.0" else bind_ip + try: + self._tcp_site = web.TCPSite(self._apprunner, host=host, port=bind_port) + await self._tcp_site.start() + except OSError: + if host is None: + raise + # the configured interface is not available, retry on all interfaces + self.logger.error( + "Could not bind to %s, will start on all interfaces as fallback!", host + ) + self._tcp_site = web.TCPSite(self._apprunner, host=None, port=bind_port) + await self._tcp_site.start() + + async def close(self) -> None: + """Cleanup on exit.""" + # stop/clean webserver + await self._tcp_site.stop() + await self._apprunner.cleanup() + await self._webapp.shutdown() + await self._webapp.cleanup() + + @property + def base_url(self): + """Return the base URL of this webserver.""" + return self._base_url + + @property + def port(self): + """Return the port of this webserver.""" + return self._bind_port + + def register_dynamic_route(self, path: str, handler: Awaitable, method: str = "*") -> Callable: + """Register a dynamic route on the webserver, returns handler to unregister.""" + if self._dynamic_routes is None: + msg = "Dynamic routes are not enabled" + raise RuntimeError(msg) + key = f"{method}.{path}" + if key in self._dynamic_routes: + msg = f"Route {path} already registered." + raise RuntimeError(msg) + self._dynamic_routes[key] = handler + + def _remove(): + return self._dynamic_routes.pop(key) + + return _remove + + def unregister_dynamic_route(self, path: str, method: str = "*") -> None: + """Unregister a dynamic route from the webserver.""" + if self._dynamic_routes is None: + msg = "Dynamic routes are not enabled" + raise RuntimeError(msg) + key = f"{method}.{path}" + self._dynamic_routes.pop(key) + + async def serve_static(self, file_path: str, request: web.Request) -> web.FileResponse: + """Serve file response.""" + headers = {"Cache-Control": "no-cache"} + return web.FileResponse(file_path, headers=headers) + + async def _handle_catch_all(self, request: web.Request) -> web.Response: + """Redirect request to correct destination.""" + # find handler for the request + for key in (f"{request.method}.{request.path}", f"*.{request.path}"): + if handler := self._dynamic_routes.get(key): + return await handler(request) + # deny all other requests + self.logger.warning( + "Received unhandled %s request to %s from %s\nheaders: %s\n", + request.method, + request.path, + request.remote, + request.headers, + ) + return web.Response(status=404) diff --git a/music_assistant/mass.py b/music_assistant/mass.py new file mode 100644 index 00000000..bb4119da --- /dev/null +++ b/music_assistant/mass.py @@ -0,0 +1,772 @@ +"""Main Music Assistant class.""" + +from __future__ import annotations + +import asyncio +import logging +import os +from collections.abc import Awaitable, Callable, Coroutine +from typing import TYPE_CHECKING, Any, Self +from uuid import uuid4 + +import aiofiles +from aiofiles.os import wrap +from aiohttp import ClientSession, TCPConnector +from music_assistant_models.api import ServerInfoMessage +from music_assistant_models.enums import EventType, ProviderType +from music_assistant_models.errors import MusicAssistantError, SetupFailedError +from music_assistant_models.event import MassEvent +from music_assistant_models.helpers.global_cache import set_global_cache_values +from music_assistant_models.provider import ProviderManifest +from zeroconf import IPVersion, NonUniqueNameException, ServiceStateChange, Zeroconf +from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf + +from music_assistant.constants import ( + API_SCHEMA_VERSION, + CONF_PROVIDERS, + CONF_SERVER_ID, + CONFIGURABLE_CORE_CONTROLLERS, + MASS_LOGGER_NAME, + MIN_SCHEMA_VERSION, + VERBOSE_LOG_LEVEL, +) +from music_assistant.controllers.cache import CacheController +from music_assistant.controllers.config import ConfigController +from music_assistant.controllers.metadata import MetaDataController +from music_assistant.controllers.music import MusicController +from music_assistant.controllers.player_queues import PlayerQueuesController +from music_assistant.controllers.players import PlayerController +from music_assistant.controllers.streams import StreamsController +from music_assistant.controllers.webserver import WebserverController +from music_assistant.helpers.api import APICommandHandler, api_command +from music_assistant.helpers.images import get_icon_string +from music_assistant.helpers.util import ( + TaskManager, + get_ip_pton, + get_package_version, + is_hass_supervisor, + load_provider_module, +) +from music_assistant.models import ProviderInstanceType + +if TYPE_CHECKING: + from types import TracebackType + + from music_assistant_models.config_entries import ProviderConfig + + from music_assistant.models.core_controller import CoreController + +isdir = wrap(os.path.isdir) +isfile = wrap(os.path.isfile) + +EventCallBackType = Callable[[MassEvent], None] +EventSubscriptionType = tuple[ + EventCallBackType, tuple[EventType, ...] | None, tuple[str, ...] | None +] + +ENABLE_DEBUG = os.environ.get("PYTHONDEVMODE") == "1" +LOGGER = logging.getLogger(MASS_LOGGER_NAME) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +PROVIDERS_PATH = os.path.join(BASE_DIR, "providers") + + +class MusicAssistant: + """Main MusicAssistant (Server) object.""" + + loop: asyncio.AbstractEventLoop + http_session: ClientSession + aiozc: AsyncZeroconf + config: ConfigController + webserver: WebserverController + cache: CacheController + metadata: MetaDataController + music: MusicController + players: PlayerController + player_queues: PlayerQueuesController + streams: StreamsController + _aiobrowser: AsyncServiceBrowser + + def __init__(self, storage_path: str, safe_mode: bool = False) -> None: + """Initialize the MusicAssistant Server.""" + self.storage_path = storage_path + self.safe_mode = safe_mode + # we dynamically register command handlers which can be consumed by the apis + self.command_handlers: dict[str, APICommandHandler] = {} + self._subscribers: set[EventSubscriptionType] = set() + self._provider_manifests: dict[str, ProviderManifest] = {} + self._providers: dict[str, ProviderInstanceType] = {} + self._tracked_tasks: dict[str, asyncio.Task] = {} + self._tracked_timers: dict[str, asyncio.TimerHandle] = {} + self.closing = False + self.running_as_hass_addon: bool = False + self.version: str = "0.0.0" + + async def start(self) -> None: + """Start running the Music Assistant server.""" + self.loop = asyncio.get_running_loop() + self.running_as_hass_addon = await is_hass_supervisor() + self.version = await get_package_version("music_assistant") or "0.0.0" + # create shared zeroconf instance + # TODO: enumerate interfaces and enable IPv6 support + self.aiozc = AsyncZeroconf(ip_version=IPVersion.V4Only) + # create shared aiohttp ClientSession + self.http_session = ClientSession( + loop=self.loop, + connector=TCPConnector( + ssl=False, + enable_cleanup_closed=True, + limit=4096, + limit_per_host=100, + ), + ) + # setup config controller first and fetch important config values + self.config = ConfigController(self) + await self.config.setup() + LOGGER.info( + "Starting Music Assistant Server (%s) version %s - HA add-on: %s - Safe mode: %s", + self.server_id, + self.version, + self.running_as_hass_addon, + self.safe_mode, + ) + # setup other core controllers + self.cache = CacheController(self) + self.webserver = WebserverController(self) + self.metadata = MetaDataController(self) + self.music = MusicController(self) + self.players = PlayerController(self) + self.player_queues = PlayerQueuesController(self) + self.streams = StreamsController(self) + # add manifests for core controllers + for controller_name in CONFIGURABLE_CORE_CONTROLLERS: + controller: CoreController = getattr(self, controller_name) + self._provider_manifests[controller.domain] = controller.manifest + await self.cache.setup(await self.config.get_core_config("cache")) + await self.music.setup(await self.config.get_core_config("music")) + await self.metadata.setup(await self.config.get_core_config("metadata")) + await self.players.setup(await self.config.get_core_config("players")) + await self.player_queues.setup(await self.config.get_core_config("player_queues")) + # load streams and webserver last so the api/frontend is + # not yet available while we're starting (or performing migrations) + self._register_api_commands() + await self.streams.setup(await self.config.get_core_config("streams")) + await self.webserver.setup(await self.config.get_core_config("webserver")) + # load all available providers from manifest files + await self.__load_provider_manifests() + # setup discovery + await self._setup_discovery() + # load providers + if not self.safe_mode: + await self._load_providers() + + async def stop(self) -> None: + """Stop running the music assistant server.""" + LOGGER.info("Stop called, cleaning up...") + self.signal_event(EventType.SHUTDOWN) + self.closing = True + # cancel all running tasks + for task in self._tracked_tasks.values(): + task.cancel() + # cleanup all providers + await asyncio.gather( + *[self.unload_provider(prov_id) for prov_id in list(self._providers.keys())], + return_exceptions=True, + ) + # stop core controllers + await self.streams.close() + await self.webserver.close() + await self.metadata.close() + await self.music.close() + await self.player_queues.close() + await self.players.close() + # cleanup cache and config + await self.config.close() + await self.cache.close() + # close/cleanup shared http session + if self.http_session: + await self.http_session.close() + + @property + def server_id(self) -> str: + """Return unique ID of this server.""" + if not self.config.initialized: + return "" + return self.config.get(CONF_SERVER_ID) # type: ignore[no-any-return] + + @api_command("info") + def get_server_info(self) -> ServerInfoMessage: + """Return Info of this server.""" + return ServerInfoMessage( + server_id=self.server_id, + server_version=self.version, + schema_version=API_SCHEMA_VERSION, + min_supported_schema_version=MIN_SCHEMA_VERSION, + base_url=self.webserver.base_url, + homeassistant_addon=self.running_as_hass_addon, + onboard_done=self.config.onboard_done, + ) + + @api_command("providers/manifests") + def get_provider_manifests(self) -> list[ProviderManifest]: + """Return all Provider manifests.""" + return list(self._provider_manifests.values()) + + @api_command("providers/manifests/get") + def get_provider_manifest(self, domain: str) -> ProviderManifest: + """Return Provider manifests of single provider(domain).""" + return self._provider_manifests[domain] + + @api_command("providers") + def get_providers( + self, provider_type: ProviderType | None = None + ) -> list[ProviderInstanceType]: + """Return all loaded/running Providers (instances), optionally filtered by ProviderType.""" + return [ + x for x in self._providers.values() if provider_type is None or provider_type == x.type + ] + + @api_command("logging/get") + async def get_application_log(self) -> str: + """Return the application log from file.""" + logfile = os.path.join(self.storage_path, "musicassistant.log") + async with aiofiles.open(logfile) as _file: + return await _file.read() + + @property + def providers(self) -> list[ProviderInstanceType]: + """Return all loaded/running Providers (instances).""" + return list(self._providers.values()) + + def get_provider( + self, provider_instance_or_domain: str, return_unavailable: bool = False + ) -> ProviderInstanceType | None: + """Return provider by instance id or domain.""" + # lookup by instance_id first + if prov := self._providers.get(provider_instance_or_domain): + if return_unavailable or prov.available: + return prov + if not getattr(prov, "is_streaming_provider", None): + # no need to lookup other instances because this provider has unique data + return None + provider_instance_or_domain = prov.domain + # fallback to match on domain + for prov in self._providers.values(): + if prov.domain != provider_instance_or_domain: + continue + if return_unavailable or prov.available: + return prov + return None + + def signal_event( + self, + event: EventType, + object_id: str | None = None, + data: Any = None, + ) -> None: + """Signal event to subscribers.""" + if self.closing: + return + + if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL): + # do not log queue time updated events because that is too chatty + LOGGER.getChild("event").log(VERBOSE_LOG_LEVEL, "%s %s", event.value, object_id or "") + + event_obj = MassEvent(event=event, object_id=object_id, data=data) + for cb_func, event_filter, id_filter in self._subscribers: + if not (event_filter is None or event in event_filter): + continue + if not (id_filter is None or object_id in id_filter): + continue + if asyncio.iscoroutinefunction(cb_func): + asyncio.run_coroutine_threadsafe(cb_func(event_obj), self.loop) + else: + self.loop.call_soon_threadsafe(cb_func, event_obj) + + def subscribe( + self, + cb_func: EventCallBackType, + event_filter: EventType | tuple[EventType, ...] | None = None, + id_filter: str | tuple[str, ...] | None = None, + ) -> Callable: + """Add callback to event listeners. + + Returns function to remove the listener. + :param cb_func: callback function or coroutine + :param event_filter: Optionally only listen for these events + :param id_filter: Optionally only listen for these id's (player_id, queue_id, uri) + """ + if isinstance(event_filter, EventType): + event_filter = (event_filter,) + if isinstance(id_filter, str): + id_filter = (id_filter,) + listener = (cb_func, event_filter, id_filter) + self._subscribers.add(listener) + + def remove_listener() -> None: + self._subscribers.remove(listener) + + return remove_listener + + def create_task( + self, + target: Coroutine | Awaitable | Callable, + *args: Any, + task_id: str | None = None, + abort_existing: bool = False, + **kwargs: Any, + ) -> asyncio.Task | asyncio.Future: + """Create Task on (main) event loop from Coroutine(function). + + Tasks created by this helper will be properly cancelled on stop. + """ + if target is None: + msg = "Target is missing" + raise RuntimeError(msg) + if task_id and (existing := self._tracked_tasks.get(task_id)) and not existing.done(): + # prevent duplicate tasks if task_id is given and already present + if abort_existing: + existing.cancel() + else: + return existing + if asyncio.iscoroutinefunction(target): + # coroutine function + task = self.loop.create_task(target(*args, **kwargs)) + elif asyncio.iscoroutine(target): + # coroutine + task = self.loop.create_task(target) + else: + task = self.loop.create_task(asyncio.to_thread(target, *args, **kwargs)) + + def task_done_callback(_task: asyncio.Task) -> None: + _task_id = task.task_id + self._tracked_tasks.pop(_task_id, None) + # log unhandled exceptions + if ( + LOGGER.isEnabledFor(logging.DEBUG) + and not _task.cancelled() + and (err := _task.exception()) + ): + task_name = _task.get_name() if hasattr(_task, "get_name") else str(_task) + LOGGER.warning( + "Exception in task %s - target: %s: %s", + task_name, + str(target), + str(err), + exc_info=err if LOGGER.isEnabledFor(logging.DEBUG) else None, + ) + + if task_id is None: + task_id = uuid4().hex + task.task_id = task_id + self._tracked_tasks[task_id] = task + task.add_done_callback(task_done_callback) + return task + + def call_later( + self, + delay: float, + target: Coroutine | Awaitable | Callable, + *args: Any, + task_id: str | None = None, + **kwargs: Any, + ) -> asyncio.TimerHandle: + """ + Run callable/awaitable after given delay. + + Use task_id for debouncing. + """ + if not task_id: + task_id = uuid4().hex + + if existing := self._tracked_timers.get(task_id): + existing.cancel() + + def _create_task() -> None: + self._tracked_timers.pop(task_id) + self.create_task(target, *args, task_id=task_id, abort_existing=True, **kwargs) + + handle = self.loop.call_later(delay, _create_task) + self._tracked_timers[task_id] = handle + return handle + + def get_task(self, task_id: str) -> asyncio.Task: + """Get existing scheduled task.""" + if existing := self._tracked_tasks.get(task_id): + # prevent duplicate tasks if task_id is given and already present + return existing + msg = "Task does not exist" + raise KeyError(msg) + + def register_api_command( + self, + command: str, + handler: Callable, + ) -> None: + """ + Dynamically register a command on the API. + + Returns handle to unregister. + """ + if command in self.command_handlers: + msg = f"Command {command} is already registered" + raise RuntimeError(msg) + self.command_handlers[command] = APICommandHandler.parse(command, handler) + + def unregister() -> None: + self.command_handlers.pop(command) + + return unregister + + async def load_provider_config( + self, + prov_conf: ProviderConfig, + ) -> None: + """Try to load a provider and catch errors.""" + # cancel existing (re)load timer if needed + task_id = f"load_provider_{prov_conf.instance_id}" + if existing := self._tracked_timers.pop(task_id, None): + existing.cancel() + + await self._load_provider(prov_conf) + + # (re)load any dependants + prov_configs = await self.config.get_provider_configs(include_values=True) + for dep_prov_conf in prov_configs: + if not dep_prov_conf.enabled: + continue + manifest = self.get_provider_manifest(dep_prov_conf.domain) + if not manifest.depends_on: + continue + if manifest.depends_on == prov_conf.domain: + await self._load_provider(dep_prov_conf) + + async def load_provider( + self, + instance_id: str, + allow_retry: bool = False, + ) -> None: + """Try to load a provider and catch errors.""" + try: + prov_conf = await self.config.get_provider_config(instance_id) + except KeyError: + # Was deleted before we could run + return + + if not prov_conf.enabled: + # Was disabled before we could run + return + + # cancel existing (re)load timer if needed + task_id = f"load_provider_{instance_id}" + if existing := self._tracked_timers.pop(task_id, None): + existing.cancel() + + try: + await self.load_provider_config(prov_conf) + except Exception as exc: + # if loading failed, we store the error in the config object + # so we can show something useful to the user + prov_conf.last_error = str(exc) + self.config.set(f"{CONF_PROVIDERS}/{instance_id}/last_error", str(exc)) + + # auto schedule a retry if the (re)load failed (handled exceptions only) + if isinstance(exc, MusicAssistantError) and allow_retry: + self.call_later( + 120, + self.load_provider, + instance_id, + allow_retry, + task_id=task_id, + ) + LOGGER.warning( + "Error loading provider(instance) %s: %s (will be retried later)", + prov_conf.name or prov_conf.instance_id, + str(exc) or exc.__class__.__name__, + # log full stack trace if verbose logging is enabled + exc_info=exc if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL) else None, + ) + return + # raise in all other situations + raise + + # (re)load any dependents if needed + for dep_prov in self.providers: + if dep_prov.available: + continue + if dep_prov.manifest.depends_on == prov_conf.domain: + await self.unload_provider(dep_prov.instance_id) + + async def unload_provider(self, instance_id: str) -> None: + """Unload a provider.""" + if provider := self._providers.get(instance_id): + # remove mdns discovery if needed + if provider.manifest.mdns_discovery: + for mdns_type in provider.manifest.mdns_discovery: + self._aiobrowser.types.discard(mdns_type) + # make sure to stop any running sync tasks first + for sync_task in self.music.in_progress_syncs: + if sync_task.provider_instance == instance_id: + sync_task.task.cancel() + # check if there are no other providers dependent of this provider + for dep_prov in self.providers: + if dep_prov.manifest.depends_on == provider.domain: + await self.unload_provider(dep_prov.instance_id) + if provider.type == ProviderType.PLAYER: + # mark all players of this provider as unavailable + for player in provider.players: + player.available = False + self.players.update(player.player_id) + try: + await provider.unload() + except Exception as err: + LOGGER.warning("Error while unload provider %s: %s", provider.name, str(err)) + finally: + self._providers.pop(instance_id, None) + await self._update_available_providers_cache() + self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers()) + + def _register_api_commands(self) -> None: + """Register all methods decorated as api_command within a class(instance).""" + for cls in ( + self, + self.config, + self.metadata, + self.music, + self.players, + self.player_queues, + ): + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + obj = getattr(cls, attr_name) + if hasattr(obj, "api_cmd"): + # method is decorated with our api decorator + self.register_api_command(obj.api_cmd, obj) + + async def _load_providers(self) -> None: + """Load providers from config.""" + # create default config for any 'builtin' providers (e.g. URL provider) + for prov_manifest in self._provider_manifests.values(): + if not prov_manifest.builtin: + continue + await self.config.create_builtin_provider_config(prov_manifest.domain) + + # load all configured (and enabled) providers + prov_configs = await self.config.get_provider_configs(include_values=True) + for prov_conf in prov_configs: + if not prov_conf.enabled: + continue + # Use a task so we can load multiple providers at once. + # If a provider fails, that will not block the loading of other providers. + self.create_task(self.load_provider(prov_conf.instance_id, allow_retry=True)) + + async def _load_provider(self, conf: ProviderConfig) -> None: + """Load (or reload) a provider.""" + # if provider is already loaded, stop and unload it first + await self.unload_provider(conf.instance_id) + LOGGER.debug("Loading provider %s", conf.name or conf.domain) + if not conf.enabled: + msg = "Provider is disabled" + raise SetupFailedError(msg) + + # validate config + try: + conf.validate() + except (KeyError, ValueError, AttributeError, TypeError) as err: + msg = "Configuration is invalid" + raise SetupFailedError(msg) from err + + domain = conf.domain + prov_manifest = self._provider_manifests.get(domain) + # check for other instances of this provider + existing = next((x for x in self.providers if x.domain == domain), None) + if existing and not prov_manifest.multi_instance: + msg = f"Provider {domain} already loaded and only one instance allowed." + raise SetupFailedError(msg) + # check valid manifest (just in case) + if not prov_manifest: + msg = f"Provider {domain} manifest not found" + raise SetupFailedError(msg) + + # handle dependency on other provider + if prov_manifest.depends_on and not self.get_provider(prov_manifest.depends_on): + # we can safely ignore this completely as the setup will be retried later + # automatically when the dependency is loaded + return + + # try to setup the module + prov_mod = await load_provider_module(domain, prov_manifest.requirements) + try: + async with asyncio.timeout(30): + provider = await prov_mod.setup(self, prov_manifest, conf) + except TimeoutError as err: + msg = f"Provider {domain} did not load within 30 seconds" + raise SetupFailedError(msg) from err + + self._providers[provider.instance_id] = provider + # run async setup + await provider.handle_async_init() + + # if we reach this point, the provider loaded successfully + LOGGER.info( + "Loaded %s provider %s", + provider.type.value, + conf.name or conf.domain, + ) + provider.available = True + + self.create_task(provider.loaded_in_mass()) + self.config.set(f"{CONF_PROVIDERS}/{conf.instance_id}/last_error", None) + self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers()) + await self._update_available_providers_cache() + + async def __load_provider_manifests(self) -> None: + """Preload all available provider manifest files.""" + + async def load_provider_manifest(provider_domain: str, provider_path: str) -> None: + """Preload all available provider manifest files.""" + # get files in subdirectory + for file_str in os.listdir(provider_path): + file_path = os.path.join(provider_path, file_str) + if not await isfile(file_path): + continue + if file_str != "manifest.json": + continue + try: + provider_manifest: ProviderManifest = await ProviderManifest.parse(file_path) + # check for icon.svg file + if not provider_manifest.icon_svg: + icon_path = os.path.join(provider_path, "icon.svg") + if await isfile(icon_path): + provider_manifest.icon_svg = await get_icon_string(icon_path) + # check for dark_icon file + if not provider_manifest.icon_svg_dark: + icon_path = os.path.join(provider_path, "icon_dark.svg") + if await isfile(icon_path): + provider_manifest.icon_svg_dark = await get_icon_string(icon_path) + self._provider_manifests[provider_manifest.domain] = provider_manifest + LOGGER.debug("Loaded manifest for provider %s", provider_manifest.name) + except Exception as exc: + LOGGER.exception( + "Error while loading manifest for provider %s", + provider_domain, + exc_info=exc, + ) + + async with TaskManager(self) as tg: + for dir_str in os.listdir(PROVIDERS_PATH): + if dir_str.startswith(("_", ".")): + continue + dir_path = os.path.join(PROVIDERS_PATH, dir_str) + if dir_str == "test" and not ENABLE_DEBUG: + continue + if not await isdir(dir_path): + continue + tg.create_task(load_provider_manifest(dir_str, dir_path)) + + async def _setup_discovery(self) -> None: + """Handle setup of MDNS discovery.""" + # create a global mdns browser + all_types: set[str] = set() + for prov_manifest in self._provider_manifests.values(): + if prov_manifest.mdns_discovery: + all_types.update(prov_manifest.mdns_discovery) + self._aiobrowser = AsyncServiceBrowser( + self.aiozc.zeroconf, + list(all_types), + handlers=[self._on_mdns_service_state_change], + ) + # register MA itself on mdns to be discovered + zeroconf_type = "_mass._tcp.local." + server_id = self.server_id + LOGGER.debug("Starting Zeroconf broadcast...") + info = AsyncServiceInfo( + zeroconf_type, + name=f"{server_id}.{zeroconf_type}", + addresses=[await get_ip_pton(self.webserver.publish_ip)], + port=self.webserver.publish_port, + properties=self.get_server_info().to_dict(), + server="mass.local.", + ) + try: + existing = getattr(self, "mass_zc_service_set", None) + if existing: + await self.aiozc.async_update_service(info) + else: + await self.aiozc.async_register_service(info) + self.mass_zc_service_set = True + except NonUniqueNameException: + LOGGER.error( + "Music Assistant instance with identical name present in the local network!" + ) + + def _on_mdns_service_state_change( + self, + zeroconf: Zeroconf, + service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: + """Handle MDNS service state callback.""" + + async def process_mdns_state_change(prov: ProviderInstanceType): + if state_change == ServiceStateChange.Removed: + info = None + else: + info = AsyncServiceInfo(service_type, name) + await info.async_request(zeroconf, 3000) + await prov.on_mdns_service_state_change(name, state_change, info) + + LOGGER.log( + VERBOSE_LOG_LEVEL, + "Service %s of type %s state changed: %s", + name, + service_type, + state_change, + ) + for prov in self._providers.values(): + if not prov.manifest.mdns_discovery: + continue + if not prov.available: + continue + if service_type in prov.manifest.mdns_discovery: + self.create_task(process_mdns_state_change(prov)) + + async def __aenter__(self) -> Self: + """Return Context manager.""" + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit context manager.""" + await self.stop() + + async def _update_available_providers_cache(self) -> None: + """Update the global cache variable of loaded/available providers.""" + await set_global_cache_values( + { + "provider_domains": {x.domain for x in self.providers}, + "provider_instance_ids": {x.instance_id for x in self.providers}, + "available_providers": { + *{x.domain for x in self.providers}, + *{x.instance_id for x in self.providers}, + }, + "unique_providers": {x.lookup_key for x in self.providers}, + "streaming_providers": { + x.lookup_key + for x in self.providers + if x.type == ProviderType.MUSIC and x.is_streaming_provider + }, + "non_streaming_providers": { + x.lookup_key + for x in self.providers + if not (x.type == ProviderType.MUSIC and x.is_streaming_provider) + }, + } + ) diff --git a/music_assistant/models/__init__.py b/music_assistant/models/__init__.py new file mode 100644 index 00000000..ce2bab18 --- /dev/null +++ b/music_assistant/models/__init__.py @@ -0,0 +1,44 @@ +"""Server specific/only models.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol + +from .metadata_provider import MetadataProvider +from .music_provider import MusicProvider +from .player_provider import PlayerProvider +from .plugin import PluginProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + + +ProviderInstanceType = MetadataProvider | MusicProvider | PlayerProvider | PluginProvider + + +class ProviderModuleType(Protocol): + """Model for a provider module to support type hints.""" + + @staticmethod + async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig + ) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + + @staticmethod + async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ diff --git a/music_assistant/models/core_controller.py b/music_assistant/models/core_controller.py new file mode 100644 index 00000000..7f2532c3 --- /dev/null +++ b/music_assistant/models/core_controller.py @@ -0,0 +1,75 @@ +"""Model/base for a Core controller within Music Assistant.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from music_assistant_models.enums import ProviderType +from music_assistant_models.provider import ProviderManifest + +from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, CoreConfig + + from music_assistant import MusicAssistant + + +class CoreController: + """Base representation of a Core controller within Music Assistant.""" + + domain: str # used as identifier (=name of the module) + manifest: ProviderManifest # some info for the UI only + + def __init__(self, mass: MusicAssistant) -> None: + """Initialize MusicProvider.""" + self.mass = mass + self._set_logger() + self.manifest = ProviderManifest( + type=ProviderType.CORE, + domain=self.domain, + name=f"{self.domain.title()} Core controller", + description=f"{self.domain.title()} Core controller", + codeowners=["@music-assistant"], + icon="puzzle-outline", + ) + + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + return () + + async def setup(self, config: CoreConfig) -> None: + """Async initialize of module.""" + + async def close(self) -> None: + """Handle logic on server stop.""" + + async def reload(self, config: CoreConfig | None = None) -> None: + """Reload this core controller.""" + await self.close() + if config is None: + config = await self.mass.config.get_core_config(self.domain) + log_level = config.get_value(CONF_LOG_LEVEL) + self._set_logger(log_level) + await self.setup(config) + + def _set_logger(self, log_level: str | None = None) -> None: + """Set the logger settings.""" + mass_logger = logging.getLogger(MASS_LOGGER_NAME) + self.logger = mass_logger.getChild(self.domain) + if log_level is None: + log_level = self.mass.config.get_raw_core_config_value( + self.domain, CONF_LOG_LEVEL, "GLOBAL" + ) + if log_level == "GLOBAL": + self.logger.setLevel(mass_logger.level) + else: + self.logger.setLevel(log_level) + if logging.getLogger().level > self.logger.level: + # if the root logger's level is higher, we need to adjust that too + logging.getLogger().setLevel(self.logger.level) diff --git a/music_assistant/models/metadata_provider.py b/music_assistant/models/metadata_provider.py new file mode 100644 index 00000000..25f28aa7 --- /dev/null +++ b/music_assistant/models/metadata_provider.py @@ -0,0 +1,56 @@ +"""Model/base for a Metadata Provider implementation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.enums import ProviderFeature + +from .provider import Provider + +if TYPE_CHECKING: + from music_assistant_models.media_items import Album, Artist, MediaItemMetadata, Track + +# ruff: noqa: ARG001, ARG002 + +DEFAULT_SUPPORTED_FEATURES = ( + ProviderFeature.ARTIST_METADATA, + ProviderFeature.ALBUM_METADATA, + ProviderFeature.TRACK_METADATA, +) + + +class MetadataProvider(Provider): + """Base representation of a Metadata Provider (controller). + + Metadata Provider implementations should inherit from this base model. + """ + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return DEFAULT_SUPPORTED_FEATURES + + async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: + """Retrieve metadata for an artist on this Metadata provider.""" + if ProviderFeature.ARTIST_METADATA in self.supported_features: + raise NotImplementedError + + async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: + """Retrieve metadata for an album on this Metadata provider.""" + if ProviderFeature.ALBUM_METADATA in self.supported_features: + raise NotImplementedError + + async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None: + """Retrieve metadata for a track on this Metadata provider.""" + if ProviderFeature.TRACK_METADATA in self.supported_features: + raise NotImplementedError + + async def resolve_image(self, path: str) -> str | bytes: + """ + Resolve an image from an image path. + + This either returns (a generator to get) raw bytes of the image or + a string with an http(s) URL or local path that is accessible from the server. + """ + return path diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py new file mode 100644 index 00000000..5fa57be5 --- /dev/null +++ b/music_assistant/models/music_provider.py @@ -0,0 +1,566 @@ +"""Model/base for a Music Provider implementation.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Sequence +from typing import TYPE_CHECKING, cast + +from music_assistant_models.enums import CacheCategory, MediaType, ProviderFeature +from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError +from music_assistant_models.media_items import ( + Album, + Artist, + BrowseFolder, + ItemMapping, + MediaItemType, + Playlist, + Radio, + SearchResults, + Track, +) + +from .provider import Provider + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.streamdetails import StreamDetails + +# ruff: noqa: ARG001, ARG002 + + +class MusicProvider(Provider): + """Base representation of a Music Provider (controller). + + Music Provider implementations should inherit from this base model. + """ + + @property + def is_streaming_provider(self) -> bool: + """ + Return True if the provider is a streaming provider. + + This literally means that the catalog is not the same as the library contents. + For local based providers (files, plex), the catalog is the same as the library content. + It also means that data is if this provider is NOT a streaming provider, + data cross instances is unique, the catalog and library differs per instance. + + Setting this to True will only query one instance of the provider for search and lookups. + Setting this to False will query all instances of this provider for search and lookups. + """ + return True + + @property + def lookup_key(self) -> str: + """Return domain if (multi-instance) streaming_provider or instance_id otherwise.""" + if self.is_streaming_provider or not self.manifest.multi_instance: + return self.domain + return self.instance_id + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + self.mass.music.start_sync(providers=[self.instance_id]) + + async def search( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 5, + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + if ProviderFeature.SEARCH in self.supported_features: + raise NotImplementedError + return SearchResults() + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve library artists from the provider.""" + if ProviderFeature.LIBRARY_ARTISTS in self.supported_features: + raise NotImplementedError + yield # type: ignore + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve library albums from the provider.""" + if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: + raise NotImplementedError + yield # type: ignore + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + if ProviderFeature.LIBRARY_TRACKS in self.supported_features: + raise NotImplementedError + yield # type: ignore + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve library/subscribed playlists from the provider.""" + if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: + raise NotImplementedError + yield # type: ignore + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + if ProviderFeature.LIBRARY_RADIOS in self.supported_features: + raise NotImplementedError + yield # type: ignore + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + raise NotImplementedError + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get a list of all albums for the given artist.""" + if ProviderFeature.ARTIST_ALBUMS in self.supported_features: + raise NotImplementedError + return [] + + async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: + """Get a list of most popular tracks for the given artist.""" + if ProviderFeature.ARTIST_TOPTRACKS in self.supported_features: + raise NotImplementedError + return [] + + async def get_album(self, prov_album_id: str) -> Album: # type: ignore[return] + """Get full album details by id.""" + if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: + raise NotImplementedError + + async def get_track(self, prov_track_id: str) -> Track: # type: ignore[return] + """Get full track details by id.""" + if ProviderFeature.LIBRARY_TRACKS in self.supported_features: + raise NotImplementedError + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[return] + """Get full playlist details by id.""" + if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: + raise NotImplementedError + + async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] + """Get full radio details by id.""" + if ProviderFeature.LIBRARY_RADIOS in self.supported_features: + raise NotImplementedError + + async def get_album_tracks( + self, + prov_album_id: str, # type: ignore[return] + ) -> list[Track]: + """Get album tracks for given album id.""" + if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: + raise NotImplementedError + + async def get_playlist_tracks( + self, + prov_playlist_id: str, + page: int = 0, + ) -> list[Track]: + """Get all playlist tracks for given playlist id.""" + if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: + raise NotImplementedError + + async def library_add(self, item: MediaItemType) -> bool: + """Add item to provider's library. Return true on success.""" + if ( + item.media_type == MediaType.ARTIST + and ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + item.media_type == MediaType.ALBUM + and ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + item.media_type == MediaType.TRACK + and ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + item.media_type == MediaType.PLAYLIST + and ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + item.media_type == MediaType.RADIO + and ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features + ): + raise NotImplementedError + self.logger.info( + "Provider %s does not support library edit, " + "the action will only be performed in the local database.", + self.name, + ) + return True + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove item from provider's library. Return true on success.""" + if ( + media_type == MediaType.ARTIST + and ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.ALBUM + and ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.TRACK + and ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.PLAYLIST + and ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.RADIO + and ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features + ): + raise NotImplementedError + self.logger.info( + "Provider %s does not support library edit, " + "the action will only be performed in the local database.", + self.name, + ) + return True + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + if ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: + raise NotImplementedError + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + if ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: + raise NotImplementedError + + async def create_playlist(self, name: str) -> Playlist: # type: ignore[return] + """Create a new playlist on provider with given name.""" + if ProviderFeature.PLAYLIST_CREATE in self.supported_features: + raise NotImplementedError + + async def get_similar_tracks( # type: ignore[return] + self, prov_track_id: str, limit: int = 25 + ) -> list[Track]: + """Retrieve a dynamic list of similar tracks based on the provided track.""" + if ProviderFeature.SIMILAR_TRACKS in self.supported_features: + raise NotImplementedError + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track/radio.""" + raise NotImplementedError + + async def get_audio_stream( # type: ignore[return] + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """ + Return the (custom) audio stream for the provider item. + + Will only be called when the stream_type is set to CUSTOM. + """ + if False: + yield + raise NotImplementedError + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + + async def resolve_image(self, path: str) -> str | bytes: + """ + Resolve an image from an image path. + + This either returns (a generator to get) raw bytes of the image or + a string with an http(s) URL or local path that is accessible from the server. + """ + return path + + async def get_item(self, media_type: MediaType, prov_item_id: str) -> MediaItemType: + """Get single MediaItem from provider.""" + if media_type == MediaType.ARTIST: + return await self.get_artist(prov_item_id) + if media_type == MediaType.ALBUM: + return await self.get_album(prov_item_id) + if media_type == MediaType.PLAYLIST: + return await self.get_playlist(prov_item_id) + if media_type == MediaType.RADIO: + return await self.get_radio(prov_item_id) + return await self.get_track(prov_item_id) + + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: + """Browse this provider's items. + + :param path: The path to browse, (e.g. provider_id://artists). + """ + if ProviderFeature.BROWSE not in self.supported_features: + # we may NOT use the default implementation if the provider does not support browse + raise NotImplementedError + + subpath = path.split("://", 1)[1] + # this reference implementation can be overridden with a provider specific approach + if subpath == "artists": + library_items = await self.mass.cache.get( + "artist", + default=[], + category=CacheCategory.LIBRARY_ITEMS, + base_key=self.instance_id, + ) + library_items = cast(list[int], library_items) + query = "artists.item_id in :ids" + query_params = {"ids": library_items} + return await self.mass.music.artists.library_items( + provider=self.instance_id, extra_query=query, extra_query_params=query_params + ) + if subpath == "albums": + library_items = await self.mass.cache.get( + "album", + default=[], + category=CacheCategory.LIBRARY_ITEMS, + base_key=self.instance_id, + ) + library_items = cast(list[int], library_items) + query = "albums.item_id in :ids" + query_params = {"ids": library_items} + return await self.mass.music.albums.library_items( + extra_query=query, extra_query_params=query_params + ) + if subpath == "tracks": + library_items = await self.mass.cache.get( + "track", + default=[], + category=CacheCategory.LIBRARY_ITEMS, + base_key=self.instance_id, + ) + library_items = cast(list[int], library_items) + query = "tracks.item_id in :ids" + query_params = {"ids": library_items} + return await self.mass.music.tracks.library_items( + extra_query=query, extra_query_params=query_params + ) + if subpath == "radios": + library_items = await self.mass.cache.get( + "radio", + default=[], + category=CacheCategory.LIBRARY_ITEMS, + base_key=self.instance_id, + ) + library_items = cast(list[int], library_items) + query = "radios.item_id in :ids" + query_params = {"ids": library_items} + return await self.mass.music.radio.library_items( + extra_query=query, extra_query_params=query_params + ) + if subpath == "playlists": + library_items = await self.mass.cache.get( + "playlist", + default=[], + category=CacheCategory.LIBRARY_ITEMS, + base_key=self.instance_id, + ) + library_items = cast(list[int], library_items) + query = "playlists.item_id in :ids" + query_params = {"ids": library_items} + return await self.mass.music.playlists.library_items( + extra_query=query, extra_query_params=query_params + ) + if subpath: + # unknown path + msg = "Invalid subpath" + raise KeyError(msg) + + # no subpath: return main listing + items: list[MediaItemType] = [] + if ProviderFeature.LIBRARY_ARTISTS in self.supported_features: + items.append( + BrowseFolder( + item_id="artists", + provider=self.domain, + path=path + "artists", + name="", + label="artists", + ) + ) + if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: + items.append( + BrowseFolder( + item_id="albums", + provider=self.domain, + path=path + "albums", + name="", + label="albums", + ) + ) + if ProviderFeature.LIBRARY_TRACKS in self.supported_features: + items.append( + BrowseFolder( + item_id="tracks", + provider=self.domain, + path=path + "tracks", + name="", + label="tracks", + ) + ) + if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: + items.append( + BrowseFolder( + item_id="playlists", + provider=self.domain, + path=path + "playlists", + name="", + label="playlists", + ) + ) + if ProviderFeature.LIBRARY_RADIOS in self.supported_features: + items.append( + BrowseFolder( + item_id="radios", + provider=self.domain, + path=path + "radios", + name="", + label="radios", + ) + ) + return items + + async def recommendations(self) -> list[MediaItemType]: + """Get this provider's recommendations. + + Returns a actual and personalised list of Media items with recommendations + form this provider for the user/account. It may return nested levels with + BrowseFolder items. + """ + if ProviderFeature.RECOMMENDATIONS in self.supported_features: + raise NotImplementedError + return [] + + async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: + """Run library sync for this provider.""" + # this reference implementation can be overridden + # with a provider specific approach if needed + for media_type in media_types: + if not self.library_supported(media_type): + continue + self.logger.debug("Start sync of %s items.", media_type.value) + controller = self.mass.music.get_controller(media_type) + cur_db_ids = set() + async for prov_item in self._get_library_gen(media_type): + library_item = await controller.get_library_item_by_prov_mappings( + prov_item.provider_mappings, + ) + try: + if not library_item and not prov_item.available: + # skip unavailable tracks + self.logger.debug( + "Skipping sync of item %s because it is unavailable", prov_item.uri + ) + continue + if not library_item: + # create full db item + # note that we skip the metadata lookup purely to speed up the sync + # the additional metadata is then lazy retrieved afterwards + if self.is_streaming_provider: + prov_item.favorite = True + library_item = await controller.add_item_to_library(prov_item) + elif getattr(library_item, "cache_checksum", None) != getattr( + prov_item, "cache_checksum", None + ): + # existing dbitem checksum changed (playlists only) + library_item = await controller.update_item_in_library( + library_item.item_id, prov_item + ) + elif library_item.available != prov_item.available: + # existing item availability changed + library_item = await controller.update_item_in_library( + library_item.item_id, prov_item + ) + cur_db_ids.add(library_item.item_id) + await asyncio.sleep(0) # yield to eventloop + except MusicAssistantError as err: + self.logger.warning( + "Skipping sync of item %s - error details: %s", prov_item.uri, str(err) + ) + + # process deletions (= no longer in library) + cache_category = CacheCategory.LIBRARY_ITEMS + cache_base_key = self.instance_id + + prev_library_items: list[int] | None + if prev_library_items := await self.mass.cache.get( + media_type.value, category=cache_category, base_key=cache_base_key + ): + for db_id in prev_library_items: + if db_id not in cur_db_ids: + try: + item = await controller.get_library_item(db_id) + except MediaNotFoundError: + # edge case: the item is already removed + continue + remaining_providers = { + x.provider_domain + for x in item.provider_mappings + if x.provider_domain != self.domain + } + if not remaining_providers and media_type != MediaType.ARTIST: + # this item is removed from the provider's library + # and we have no other providers attached to it + # it is safe to remove it from the MA library too + # note we skip artists here to prevent a recursive removal + # of all albums and tracks underneath this artist + await controller.remove_item_from_library(db_id) + else: + # otherwise: just unmark favorite + await controller.set_favorite(db_id, False) + await asyncio.sleep(0) # yield to eventloop + await self.mass.cache.set( + media_type.value, list(cur_db_ids), category=cache_category, base_key=cache_base_key + ) + + # DO NOT OVERRIDE BELOW + + def library_supported(self, media_type: MediaType) -> bool: + """Return if Library is supported for given MediaType on this provider.""" + if media_type == MediaType.ARTIST: + return ProviderFeature.LIBRARY_ARTISTS in self.supported_features + if media_type == MediaType.ALBUM: + return ProviderFeature.LIBRARY_ALBUMS in self.supported_features + if media_type == MediaType.TRACK: + return ProviderFeature.LIBRARY_TRACKS in self.supported_features + if media_type == MediaType.PLAYLIST: + return ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features + if media_type == MediaType.RADIO: + return ProviderFeature.LIBRARY_RADIOS in self.supported_features + return False + + def library_edit_supported(self, media_type: MediaType) -> bool: + """Return if Library add/remove is supported for given MediaType on this provider.""" + if media_type == MediaType.ARTIST: + return ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features + if media_type == MediaType.ALBUM: + return ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features + if media_type == MediaType.TRACK: + return ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features + if media_type == MediaType.PLAYLIST: + return ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features + if media_type == MediaType.RADIO: + return ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features + return False + + def _get_library_gen(self, media_type: MediaType) -> AsyncGenerator[MediaItemType, None]: + """Return library generator for given media_type.""" + if media_type == MediaType.ARTIST: + return self.get_library_artists() + if media_type == MediaType.ALBUM: + return self.get_library_albums() + if media_type == MediaType.TRACK: + return self.get_library_tracks() + if media_type == MediaType.PLAYLIST: + return self.get_library_playlists() + if media_type == MediaType.RADIO: + return self.get_library_radios() + raise NotImplementedError diff --git a/music_assistant/models/player_provider.py b/music_assistant/models/player_provider.py new file mode 100644 index 00000000..8d54db53 --- /dev/null +++ b/music_assistant/models/player_provider.py @@ -0,0 +1,243 @@ +"""Model/base for a Metadata Provider implementation.""" + +from __future__ import annotations + +from abc import abstractmethod +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ( + BASE_PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_ANNOUNCE_VOLUME, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN, + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, + ConfigEntry, + PlayerConfig, +) +from music_assistant_models.errors import UnsupportedFeaturedException +from zeroconf import ServiceStateChange +from zeroconf.asyncio import AsyncServiceInfo + +from .provider import Provider + +if TYPE_CHECKING: + from music_assistant_models.player import Player, PlayerMedia + +# ruff: noqa: ARG001, ARG002 + + +class PlayerProvider(Provider): + """Base representation of a Player Provider (controller). + + Player Provider implementations should inherit from this base model. + """ + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + await self.discover_players() + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + return ( + *BASE_PLAYER_CONFIG_ENTRIES, + # add default entries for announce feature + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, + CONF_ENTRY_ANNOUNCE_VOLUME, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX, + ) + + async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: + """Call (by config manager) when the configuration of a player changes.""" + # default implementation: feel free to override + if ( + "enabled" in changed_keys + and config.enabled + and not self.mass.players.get(config.player_id) + ): + # if a player gets enabled, trigger discovery + task_id = f"discover_players_{self.instance_id}" + self.mass.call_later(5, self.discover_players, task_id=task_id) + else: + await self.poll_player(config.player_id) + + @abstractmethod + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player. + + - player_id: player_id of the player to handle the command. + """ + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY (unpause) command to given player. + + - player_id: player_id of the player to handle the command. + """ + # will only be called for players with Pause feature set. + raise NotImplementedError + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player. + + - player_id: player_id of the player to handle the command. + """ + # will only be called for players with Pause feature set. + raise NotImplementedError + + @abstractmethod + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player. + + This is called by the Players controller to start playing a mediaitem on the given player. + The provider's own implementation should work out how to handle this request. + + - player_id: player_id of the player to handle the command. + - media: Details of the item that needs to be played on the player. + """ + raise NotImplementedError + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """ + Handle enqueuing of the next (queue) item on the player. + + Called when player reports it started buffering a queue item + and when the queue items updated. + + A PlayerProvider implementation is in itself responsible for handling this + so that the queue items keep playing until its empty or the player stopped. + + This will NOT be called if the end of the queue is reached (and repeat disabled). + This will NOT be called if the player is using flow mode to playback the queue. + """ + # will only be called for players with ENQUEUE feature set. + raise NotImplementedError + + async def play_announcement( + self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None + ) -> None: + """Handle (provider native) playback of an announcement on given player.""" + # will only be called for players with PLAY_ANNOUNCEMENT feature set. + raise NotImplementedError + + async def cmd_power(self, player_id: str, powered: bool) -> None: + """Send POWER command to given player. + + - player_id: player_id of the player to handle the command. + - powered: bool if player should be powered on or off. + """ + # will only be called for players with Power feature set. + raise NotImplementedError + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player. + + - player_id: player_id of the player to handle the command. + - volume_level: volume level (0..100) to set on the player. + """ + # will only be called for players with Volume feature set. + raise NotImplementedError + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player. + + - player_id: player_id of the player to handle the command. + - muted: bool if player should be muted. + """ + # will only be called for players with Mute feature set. + raise NotImplementedError + + async def cmd_seek(self, player_id: str, position: int) -> None: + """Handle SEEK command for given player. + + - player_id: player_id of the player to handle the command. + - position: position in seconds to seek to in the current playing item. + """ + # will only be called for players with Seek feature set. + raise NotImplementedError + + async def cmd_next(self, player_id: str) -> None: + """Handle NEXT TRACK command for given player.""" + # will only be called for players with 'next_previous' feature set. + raise NotImplementedError + + async def cmd_previous(self, player_id: str) -> None: + """Handle PREVIOUS TRACK command for given player.""" + # will only be called for players with 'next_previous' feature set. + raise NotImplementedError + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the sync leader. + """ + # will only be called for players with SYNC feature set. + raise NotImplementedError + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + # will only be called for players with SYNC feature set. + raise NotImplementedError + + async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: + """Create temporary sync group by joining given players to target player.""" + for child_id in child_player_ids: + # default implementation, simply call the cmd_sync for all child players + await self.cmd_sync(child_id, target_player) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates. + + This is called by the Player Manager; + if 'needs_poll' is set to True in the player object. + """ + + async def remove_player(self, player_id: str) -> None: + """Remove a player.""" + # will only be called for players with REMOVE_PLAYER feature set. + raise NotImplementedError + + async def discover_players(self) -> None: + """Discover players for this provider.""" + # This will be called (once) when the player provider is loaded into MA. + # Default implementation is mdns discovery, which will also automatically + # discovery players during runtime. If a provider overrides this method and + # doesn't use mdns, it is responsible for periodically searching for new players. + if not self.available: + return + for mdns_type in self.manifest.mdns_discovery or []: + for mdns_name in set(self.mass.aiozc.zeroconf.cache.cache): + if mdns_type not in mdns_name or mdns_type == mdns_name: + continue + info = AsyncServiceInfo(mdns_type, mdns_name) + if await info.async_request(self.mass.aiozc.zeroconf, 3000): + await self.on_mdns_service_state_change( + mdns_name, ServiceStateChange.Added, info + ) + + async def set_members(self, player_id: str, members: list[str]) -> None: + """Set members for a groupplayer.""" + # will only be called for (group)players with SET_MEMBERS feature set. + raise UnsupportedFeaturedException + + # DO NOT OVERRIDE BELOW + + @property + def players(self) -> list[Player]: + """Return all players belonging to this provider.""" + return [ + player + for player in self.mass.players + if player.provider in (self.instance_id, self.domain) + ] diff --git a/music_assistant/models/plugin.py b/music_assistant/models/plugin.py new file mode 100644 index 00000000..8c0ebc5e --- /dev/null +++ b/music_assistant/models/plugin.py @@ -0,0 +1,15 @@ +"""Model/base for a Plugin Provider implementation.""" + +from __future__ import annotations + +from .provider import Provider + +# ruff: noqa: ARG001, ARG002 + + +class PluginProvider(Provider): + """ + Base representation of a Plugin for Music Assistant. + + Plugin Provider implementations should inherit from this base model. + """ diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py new file mode 100644 index 00000000..89b13379 --- /dev/null +++ b/music_assistant/models/provider.py @@ -0,0 +1,110 @@ +"""Model/base for a Provider implementation within Music Assistant.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.enums import ProviderFeature, ProviderType + from music_assistant_models.provider import ProviderManifest + from zeroconf import ServiceStateChange + from zeroconf.asyncio import AsyncServiceInfo + + from music_assistant import MusicAssistant + + +class Provider: + """Base representation of a Provider implementation within Music Assistant.""" + + def __init__( + self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig + ) -> None: + """Initialize MusicProvider.""" + self.mass = mass + self.manifest = manifest + self.config = config + mass_logger = logging.getLogger(MASS_LOGGER_NAME) + self.logger = mass_logger.getChild(self.domain) + log_level = config.get_value(CONF_LOG_LEVEL) + if log_level == "GLOBAL": + self.logger.setLevel(mass_logger.level) + else: + self.logger.setLevel(log_level) + if logging.getLogger().level > self.logger.level: + # if the root logger's level is higher, we need to adjust that too + logging.getLogger().setLevel(self.logger.level) + self.logger.debug("Log level configured to %s", log_level) + self.cache = mass.cache + self.available = False + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return () + + @property + def lookup_key(self) -> str: + """Return instance_id if multi_instance capable or domain otherwise.""" + # should not be overridden in normal circumstances + return self.instance_id if self.manifest.multi_instance else self.domain + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + + async def on_mdns_service_state_change( + self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None + ) -> None: + """Handle MDNS service state callback.""" + + @property + def type(self) -> ProviderType: + """Return type of this provider.""" + return self.manifest.type + + @property + def domain(self) -> str: + """Return domain for this provider.""" + return self.manifest.domain + + @property + def instance_id(self) -> str: + """Return instance_id for this provider(instance).""" + return self.config.instance_id + + @property + def name(self) -> str: + """Return (custom) friendly name for this provider instance.""" + if self.config.name: + return self.config.name + inst_count = len([x for x in self.mass.music.providers if x.domain == self.domain]) + if inst_count > 1: + postfix = self.instance_id[:-8] + return f"{self.manifest.name}.{postfix}" + return self.manifest.name + + def to_dict(self, *args, **kwargs) -> dict[str, Any]: + """Return Provider(instance) as serializable dict.""" + return { + "type": self.type.value, + "domain": self.domain, + "name": self.config.name or self.name, + "instance_id": self.instance_id, + "lookup_key": self.lookup_key, + "supported_features": [x.value for x in self.supported_features], + "available": self.available, + "is_streaming_provider": getattr(self, "is_streaming_provider", None), + } diff --git a/music_assistant/providers/__init__.py b/music_assistant/providers/__init__.py new file mode 100644 index 00000000..3a28df8a --- /dev/null +++ b/music_assistant/providers/__init__.py @@ -0,0 +1 @@ +"""Package with Music Provider controllers.""" diff --git a/music_assistant/providers/_template_music_provider/__init__.py b/music_assistant/providers/_template_music_provider/__init__.py new file mode 100644 index 00000000..89cbba27 --- /dev/null +++ b/music_assistant/providers/_template_music_provider/__init__.py @@ -0,0 +1,461 @@ +""" +DEMO/TEMPLATE Music Provider for Music Assistant. + +This is an empty music provider with no actual implementation. +Its meant to get started developing a new music provider for Music Assistant. + +Use it as a reference to discover what methods exists and what they should return. +Also it is good to look at existing music providers to get a better understanding, +due to the fact that providers may be flexible and support different features. + +If you are relying on a third-party library to interact with the music source, +you can then reference your library in the manifest in the requirements section, +which is a list of (versioned!) python modules (pip syntax) that should be installed +when the provider is selected by the user. + +Please keep in mind that Music Assistant is a fully async application and all +methods should be implemented as async methods. If you are not familiar with +async programming in Python, we recommend you to read up on it first. +If you are using a third-party library that is not async, you can need to use the several +helper methods such as asyncio.to_thread or the create_task in the mass object to wrap +the calls to the library in a thread. + +To add a new provider to Music Assistant, you need to create a new folder +in the providers folder with the name of your provider (e.g. 'my_music_provider'). +In that folder you should create (at least) a __init__.py file and a manifest.json file. + +Optional is an icon.svg file that will be used as the icon for the provider in the UI, +but we also support that you specify a material design icon in the manifest.json file. + +IMPORTANT NOTE: +We strongly recommend developing on either MacOS or Linux and start your development +environment by running the setup.sh script in the scripts folder of the repository. +This will create a virtual environment and install all dependencies needed for development. +See also our general DEVELOPMENT.md guide in the repository for more information. + +""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator, Sequence +from typing import TYPE_CHECKING + +from music_assistant_models.enums import ContentType, MediaType, ProviderFeature, StreamType +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + ItemMapping, + MediaItemType, + Playlist, + ProviderMapping, + Radio, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + # setup is called when the user wants to setup a new provider instance. + # you are free to do any preflight checks here and but you must return + # an instance of the provider. + return MyDemoMusicprovider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + # Config Entries are used to configure the Music Provider if needed. + # See the models of ConfigEntry and ConfigValueType for more information what is supported. + # The ConfigEntry is a dataclass that represents a single configuration entry. + # The ConfigValueType is an Enum that represents the type of value that + # can be stored in a ConfigEntry. + # If your provider does not need any configuration, you can return an empty tuple. + + # We support flow-like configuration where you can have multiple steps of configuration + # using the 'action' parameter to distinguish between the different steps. + # The 'values' parameter contains the raw values of the config entries that were filled in + # by the user in the UI. This is a dictionary with the key being the config entry id + # and the value being the actual value filled in by the user. + + # For authentication flows where the user needs to be redirected to a login page + # or some other external service, we have a simple helper that can help you with those steps + # and a callback url that you can use to redirect the user back to the Music Assistant UI. + # See for example the Deezer provider for an example of how to use this. + return () + + +class MyDemoMusicprovider(MusicProvider): + """ + Example/demo Music provider. + + Note that this is always subclassed from MusicProvider, + which in turn is a subclass of the generic Provider model. + + The base implementation already takes care of some convenience methods, + such as the mass object and the logger. Take a look at the base class + for more information on what is available. + + Just like with any other subclass, make sure that if you override + any of the default methods (such as __init__), you call the super() method. + In most cases its not needed to override any of the builtin methods and you only + implement the abc methods with your actual implementation. + """ + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + # MANDATORY + # you should return a tuple of provider-level features + # here that your player provider supports or an empty tuple if none. + # for example 'ProviderFeature.SYNC_PLAYERS' if you can sync players. + return ( + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.RECOMMENDATIONS, + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.SIMILAR_TRACKS, + # see the ProviderFeature enum for all available features + ) + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + # OPTIONAL + # this is an optional method that you can implement if + # relevant or leave out completely if not needed. + # In most cases this can be omitted for music providers. + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + # OPTIONAL + # This is an optional method that you can implement if + # relevant or leave out completely if not needed. + # It will be called when the provider is unloaded from Music Assistant. + # for example to disconnect from a service or clean up resources. + + @property + def is_streaming_provider(self) -> bool: + """ + Return True if the provider is a streaming provider. + + This literally means that the catalog is not the same as the library contents. + For local based providers (files, plex), the catalog is the same as the library content. + It also means that data is if this provider is NOT a streaming provider, + data cross instances is unique, the catalog and library differs per instance. + + Setting this to True will only query one instance of the provider for search and lookups. + Setting this to False will query all instances of this provider for search and lookups. + """ + # For streaming providers return True here but for local file based providers return False. + return True + + async def search( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 5, + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + # OPTIONAL + # Will only be called if you reported the SEARCH feature in the supported_features. + # It allows searching your provider for media items. + # See the model for SearchResults for more information on what to return, but + # in general you should return a list of MediaItems for each media type. + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve library artists from the provider.""" + # OPTIONAL + # Will only be called if you reported the LIBRARY_ARTISTS feature + # in the supported_features and you did not override the default sync method. + # It allows retrieving the library/favorite artists from your provider. + # Warning: Async generator: + # You should yield Artist objects for each artist in the library. + yield Artist( + # A simple example of an artist object, + # you should replace this with actual data from your provider. + # Explore the Artist model for all options and descriptions. + item_id="123", + provider=self.instance_id, + name="Artist Name", + provider_mappings={ + ProviderMapping( + # A provider mapping is used to provide details about this item on this provider + # Music Assistant differentiates between domain and instance id to account for + # multiple instances of the same provider. + # The instance_id is auto generated by MA. + item_id="123", + provider_domain=self.domain, + provider_instance=self.instance_id, + # set 'available' to false if the item is (temporary) unavailable + available=True, + audio_format=AudioFormat( + # provide details here about sample rate etc. if known + content_type=ContentType.FLAC, + ), + ) + }, + ) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve library albums from the provider.""" + # OPTIONAL + # Will only be called if you reported the LIBRARY_ALBUMS feature + # in the supported_features and you did not override the default sync method. + # It allows retrieving the library/favorite albums from your provider. + # Warning: Async generator: + # You should yield Album objects for each album in the library. + yield # type: ignore + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + # OPTIONAL + # Will only be called if you reported the LIBRARY_TRACKS feature + # in the supported_features and you did not override the default sync method. + # It allows retrieving the library/favorite tracks from your provider. + # Warning: Async generator: + # You should yield Track objects for each track in the library. + yield # type: ignore + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve library/subscribed playlists from the provider.""" + # OPTIONAL + # Will only be called if you reported the LIBRARY_PLAYLISTS feature + # in the supported_features and you did not override the default sync method. + # It allows retrieving the library/favorite playlists from your provider. + # Warning: Async generator: + # You should yield Playlist objects for each playlist in the library. + yield # type: ignore + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + # OPTIONAL + # Will only be called if you reported the LIBRARY_RADIOS feature + # in the supported_features and you did not override the default sync method. + # It allows retrieving the library/favorite radio stations from your provider. + # Warning: Async generator: + # You should yield Radio objects for each radio station in the library. + yield + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + # Get full details of a single Artist. + # Mandatory only if you reported LIBRARY_ARTISTS in the supported_features. + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get a list of all albums for the given artist.""" + # Get a list of all albums for the given artist. + # Mandatory only if you reported ARTIST_ALBUMS in the supported_features. + + async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: + """Get a list of most popular tracks for the given artist.""" + # Get a list of most popular tracks for the given artist. + # Mandatory only if you reported ARTIST_TOPTRACKS in the supported_features. + # Note that (local) file based providers will simply return all artist tracks here. + + async def get_album(self, prov_album_id: str) -> Album: # type: ignore[return] + """Get full album details by id.""" + # Get full details of a single Album. + # Mandatory only if you reported LIBRARY_ALBUMS in the supported_features. + + async def get_track(self, prov_track_id: str) -> Track: # type: ignore[return] + """Get full track details by id.""" + # Get full details of a single Track. + # Mandatory only if you reported LIBRARY_TRACKS in the supported_features. + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[return] + """Get full playlist details by id.""" + # Get full details of a single Playlist. + # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported + + async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] + """Get full radio details by id.""" + # Get full details of a single Radio station. + # Mandatory only if you reported LIBRARY_RADIOS in the supported_features. + + async def get_album_tracks( + self, + prov_album_id: str, # type: ignore[return] + ) -> list[Track]: + """Get album tracks for given album id.""" + # Get all tracks for a given album. + # Mandatory only if you reported ARTIST_ALBUMS in the supported_features. + + async def get_playlist_tracks( + self, + prov_playlist_id: str, + page: int = 0, + ) -> list[Track]: + """Get all playlist tracks for given playlist id.""" + # Get all tracks for a given playlist. + # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported_features. + + async def library_add(self, item: MediaItemType) -> bool: + """Add item to provider's library. Return true on success.""" + # Add an item to your provider's library. + # This is only called if the provider supports the EDIT feature for the media type. + return True + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove item from provider's library. Return true on success.""" + # Remove an item from your provider's library. + # This is only called if the provider supports the EDIT feature for the media type. + return True + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + # Add track(s) to a playlist. + # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature. + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + # Remove track(s) from a playlist. + # This is only called if the provider supports the EDPLAYLIST_TRACKS_EDITIT feature. + + async def create_playlist(self, name: str) -> Playlist: # type: ignore[return] + """Create a new playlist on provider with given name.""" + # Create a new playlist on the provider. + # This is only called if the provider supports the PLAYLIST_CREATE feature. + + async def get_similar_tracks( # type: ignore[return] + self, prov_track_id: str, limit: int = 25 + ) -> list[Track]: + """Retrieve a dynamic list of similar tracks based on the provided track.""" + # Get a list of similar tracks based on the provided track. + # This is only called if the provider supports the SIMILAR_TRACKS feature. + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track/radio.""" + # Get stream details for a track or radio. + # Implementing this method is MANDATORY to allow playback. + # The StreamDetails contain info how Music Assistant can play the track. + # item_id will always be a track or radio id. Later, when/if MA supports + # podcasts or audiobooks, this may as well be an episode or chapter id. + # You should return a StreamDetails object here with the info as accurate as possible + # to allow Music Assistant to process the audio using ffmpeg. + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=AudioFormat( + # provide details here about sample rate etc. if known + # set content type to unknown to let ffmpeg guess the codec/container + content_type=ContentType.UNKNOWN, + ), + media_type=MediaType.TRACK, + # streamtype defines how the stream is provided + # for most providers this will be HTTP but you can also use CUSTOM + # to provide a custom stream generator in get_audio_stream. + stream_type=StreamType.HTTP, + # explore the StreamDetails model and StreamType enum for more options + # but the above should be the mandatory fields to set. + ) + + async def get_audio_stream( # type: ignore[return] + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """ + Return the (custom) audio stream for the provider item. + + Will only be called when the stream_type is set to CUSTOM. + """ + # this is an async generator that should yield raw audio bytes + # for the given streamdetails. You can use this to provide a custom + # stream generator for the audio stream. This is only called when the + # stream_type is set to CUSTOM in the get_stream_details method. + yield # type: ignore + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + # This is OPTIONAL callback that is called when an item has been streamed. + # You can use this e.g. for playback reporting or statistics. + + async def resolve_image(self, path: str) -> str | bytes: + """ + Resolve an image from an image path. + + This either returns (a generator to get) raw bytes of the image or + a string with an http(s) URL or local path that is accessible from the server. + """ + # This is an OPTIONAL method that you can implement to resolve image paths. + # This is used to resolve image paths that are returned in the MediaItems. + # You can return a URL to an image or a generator that yields the raw bytes of the image. + # This will only be called when you set 'remotely_accessible' + # to false in a MediaItemImage object. + return path + + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: + """Browse this provider's items. + + :param path: The path to browse, (e.g. provider_id://artists). + """ + # Browse your provider's recommendations/media items. + # This is only called if you reported the BROWSE feature in the supported_features. + # You should return a list of MediaItems or ItemMappings for the given path. + # Note that you can return nested levels with BrowseFolder items. + + # The MusicProvider base model has a default implementation of this method + # that will call the get_library_* methods if you did not override it. + return [] + + async def recommendations(self) -> list[MediaItemType]: + """Get this provider's recommendations. + + Returns a actual and personalised list of Media items with recommendations + form this provider for the user/account. It may return nested levels with + BrowseFolder items. + """ + # Get this provider's recommendations. + # This is only called if you reported the RECOMMENDATIONS feature in the supported_features. + return [] + + async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: + """Run library sync for this provider.""" + # Run a full sync of the library for the given media types. + # This is called by the music controller to sync items from your provider to the library. + # As a generic rule of thumb the default implementation within the MusicProvider + # base model should be sufficient for most (streaming) providers. + # If you need to do some custom sync logic, you can override this method. + # For example the filesystem provider in MA, overrides this method to scan the filesystem. diff --git a/music_assistant/providers/_template_music_provider/icon.svg b/music_assistant/providers/_template_music_provider/icon.svg new file mode 100644 index 00000000..845920ca --- /dev/null +++ b/music_assistant/providers/_template_music_provider/icon.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 25 25" version="1.1"> +<g id="surface1"> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 1.5 0 L 23.5 0 C 24.328125 0 25 0.671875 25 1.5 L 25 23.5 C 25 24.328125 24.328125 25 23.5 25 L 1.5 25 C 0.671875 25 0 24.328125 0 23.5 L 0 1.5 C 0 0.671875 0.671875 0 1.5 0 Z M 1.5 0 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 10.386719 18.875 L 14.8125 7.125 L 16.113281 7.125 L 11.6875 18.875 Z M 10.386719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 21.371094 18.875 L 16.945312 7.125 L 18.246094 7.125 L 22.671875 18.875 Z M 21.371094 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.636719 18.875 L 2.636719 7.125 L 3.875 7.125 L 3.875 18.875 Z M 2.636719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 5.445312 18.875 L 5.445312 7.125 L 6.683594 7.125 L 6.683594 18.875 Z M 5.445312 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 8.253906 18.875 L 8.253906 7.125 L 9.492188 7.125 L 9.492188 18.875 Z M 8.253906 18.875 "/> +</g> +</svg> diff --git a/music_assistant/providers/_template_music_provider/manifest.json b/music_assistant/providers/_template_music_provider/manifest.json new file mode 100644 index 00000000..15d6b83a --- /dev/null +++ b/music_assistant/providers/_template_music_provider/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "template_player_provider", + "name": "Name of the Player provider goes here", + "description": "Short description of the player provider goes here", + "codeowners": ["@yourgithubusername"], + "requirements": [], + "documentation": "Link to the documentation on the music-assistant.io helppage (may be added later).", + "mdns_discovery": ["_optional_mdns_service_type._tcp.local."] +} diff --git a/music_assistant/providers/_template_player_provider/__init__.py b/music_assistant/providers/_template_player_provider/__init__.py new file mode 100644 index 00000000..ae3cd309 --- /dev/null +++ b/music_assistant/providers/_template_player_provider/__init__.py @@ -0,0 +1,382 @@ +""" +DEMO/TEMPLATE Player Provider for Music Assistant. + +This is an empty player provider with no actual implementation. +Its meant to get started developing a new player provider for Music Assistant. + +Use it as a reference to discover what methods exists and what they should return. +Also it is good to look at existing player providers to get a better understanding, +due to the fact that providers may be flexible and support different features and/or +ways to discover players on the network. + +In general, the actual device communication should reside in a separate library. +You can then reference your library in the manifest in the requirements section, +which is a list of (versioned!) python modules (pip syntax) that should be installed +when the provider is selected by the user. + +To add a new player provider to Music Assistant, you need to create a new folder +in the providers folder with the name of your provider (e.g. 'my_player_provider'). +In that folder you should create (at least) a __init__.py file and a manifest.json file. + +Optional is an icon.svg file that will be used as the icon for the provider in the UI, +but we also support that you specify a material design icon in the manifest.json file. + +IMPORTANT NOTE: +We strongly recommend developing on either MacOS or Linux and start your development +environment by running the setup.sh scripts in the scripts folder of the repository. +This will create a virtual environment and install all dependencies needed for development. +See also our general DEVELOPMENT.md guide in the repository for more information. + +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.enums import PlayerFeature, PlayerType, ProviderFeature +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from zeroconf import ServiceStateChange + +from music_assistant.helpers.util import get_primary_ip_address_from_zeroconf +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ( + ConfigEntry, + ConfigValueType, + PlayerConfig, + ProviderConfig, + ) + from music_assistant_models.provider import ProviderManifest + from zeroconf.asyncio import AsyncServiceInfo + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + # setup is called when the user wants to setup a new provider instance. + # you are free to do any preflight checks here and but you must return + # an instance of the provider. + return MyDemoPlayerprovider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + # Config Entries are used to configure the Player Provider if needed. + # See the models of ConfigEntry and ConfigValueType for more information what is supported. + # The ConfigEntry is a dataclass that represents a single configuration entry. + # The ConfigValueType is an Enum that represents the type of value that + # can be stored in a ConfigEntry. + # If your provider does not need any configuration, you can return an empty tuple. + return () + + +class MyDemoPlayerprovider(PlayerProvider): + """ + Example/demo Player provider. + + Note that this is always subclassed from PlayerProvider, + which in turn is a subclass of the generic Provider model. + + The base implementation already takes care of some convenience methods, + such as the mass object and the logger. Take a look at the base class + for more information on what is available. + + Just like with any other subclass, make sure that if you override + any of the default methods (such as __init__), you call the super() method. + In most cases its not needed to override any of the builtin methods and you only + implement the abc methods with your actual implementation. + """ + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + # MANDATORY + # you should return a tuple of provider-level features + # here that your player provider supports or an empty tuple if none. + # for example 'ProviderFeature.SYNC_PLAYERS' if you can sync players. + return (ProviderFeature.SYNC_PLAYERS,) + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + # OPTIONAL + # this is an optional method that you can implement if + # relevant or leave out completely if not needed. + # it will be called after the provider has been fully loaded into Music Assistant. + # you can use this for instance to trigger custom (non-mdns) discovery of players + # or any other logic that needs to run after the provider is fully loaded. + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + # OPTIONAL + # this is an optional method that you can implement if + # relevant or leave out completely if not needed. + # it will be called when the provider is unloaded from Music Assistant. + # this means also when the provider is getting reloaded + + async def on_mdns_service_state_change( + self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None + ) -> None: + """Handle MDNS service state callback.""" + # MANDATORY IF YOU WANT TO USE MDNS DISCOVERY + # OPTIONAL if you dont use mdns for discovery of players + # If you specify a mdns service type in the manifest.json, this method will be called + # automatically on mdns changes for the specified service type. + + # If no mdns service type is specified, this method is omitted and you + # can completely remove it from your provider implementation. + + # NOTE: If you do not use mdns for discovery of players on the network, + # you must implement your own discovery mechanism and logic to add new players + # and update them on state changes when needed. + # Below is a bit of example implementation but we advise to look at existing + # player providers for more inspiration. + name = name.split("@", 1)[1] if "@" in name else name + player_id = info.decoded_properties["uuid"] # this is just an example! + # handle removed player + if state_change == ServiceStateChange.Removed: + # check if the player manager has an existing entry for this player + if mass_player := self.mass.players.get(player_id): + # the player has become unavailable + self.logger.debug("Player offline: %s", mass_player.display_name) + mass_player.available = False + self.mass.players.update(player_id) + return + # handle update for existing device + # (state change is either updated or added) + # check if we have an existing player in the player manager + # note that you can use this point to update the player connection info + # if that changed (e.g. ip address) + if mass_player := self.mass.players.get(player_id): + # existing player found in the player manager, + # this is an existing player that has been updated/reconnected + # or simply a re-announcement on mdns. + cur_address = get_primary_ip_address_from_zeroconf(info) + if cur_address and cur_address != mass_player.device_info.address: + self.logger.debug( + "Address updated to %s for player %s", cur_address, mass_player.display_name + ) + mass_player.device_info = DeviceInfo( + model=mass_player.device_info.model, + manufacturer=mass_player.device_info.manufacturer, + address=str(cur_address), + ) + if not mass_player.available: + # if the player was marked offline and you now receive an mdns update + # it means the player is back online and we should try to connect to it + self.logger.debug("Player back online: %s", mass_player.display_name) + # you can try to connect to the player here if needed + mass_player.available = True + # inform the player manager of any changes to the player object + # note that you would normally call this from some other callback from + # the player's native api/library which informs you of changes in the player state. + # as a last resort you can also choose to let the player manager + # poll the player for state changes + self.mass.players.update(player_id) + return + # handle new player + self.logger.debug("Discovered device %s on %s", name, cur_address) + # your own connection logic will probably be implemented here where + # you connect to the player etc. using your device/provider specific library. + + # Instantiate the MA Player object and register it with the player manager + mass_player = Player( + player_id=player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=name, + available=True, + powered=False, + device_info=DeviceInfo( + model="Model XYX", + manufacturer="Super Brand", + address=cur_address, + ), + # set the supported features for this player only with + # the ones the player actually supports + supported_features=( + PlayerFeature.POWER, # if the player can be turned on/off + PlayerFeature.VOLUME_SET, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.PLAY_ANNOUNCEMENT, # see play_announcement method + ), + ) + # register the player with the player manager + await self.mass.players.register(mass_player) + + # once the player is registered, you can either instruct the player manager to + # poll the player for state changes or you can implement your own logic to + # listen for state changes from the player and update the player object accordingly. + # in any case, you need to call the update method on the player manager: + self.mass.players.update(player_id) + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + # OPTIONAL + # this method is optional and should be implemented if you need player specific + # configuration entries. If you do not need player specific configuration entries, + # you can leave this method out completely to accept the default implementation. + # Please note that you need to call the super() method to get the default entries. + return () + + async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: + """Call (by config manager) when the configuration of a player changes.""" + # OPTIONAL + # this will be called whenever a player config changes + # you can use this to react to changes in player configuration + # but this is completely optional and you can leave it out if not needed. + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + # MANDATORY + # this method is mandatory and should be implemented. + # this method should send a stop command to the given player. + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + # MANDATORY + # this method is mandatory and should be implemented. + # this method should send a play command to the given player. + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + # OPTIONAL - required only if you specified PlayerFeature.PAUSE + # this method should send a pause command to the given player. + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + # OPTIONAL - required only if you specified PlayerFeature.VOLUME_SET + # this method should send a volume set command to the given player. + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + # OPTIONAL - required only if you specified PlayerFeature.VOLUME_MUTE + # this method should send a volume mute command to the given player. + + async def cmd_seek(self, player_id: str, position: int) -> None: + """Handle SEEK command for given queue. + + - player_id: player_id of the player to handle the command. + - position: position in seconds to seek to in the current playing item. + """ + # OPTIONAL - required only if you specified PlayerFeature.SEEK + # this method should handle the seek command for the given player. + # the position is the position in seconds to seek to in the current playing item. + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player. + + This is called by the Players controller to start playing a mediaitem on the given player. + The provider's own implementation should work out how to handle this request. + + - player_id: player_id of the player to handle the command. + - media: Details of the item that needs to be played on the player. + """ + # MANDATORY + # this method is mandatory and should be implemented. + # this method should handle the play_media command for the given player. + # It will be called when media needs to be played on the player. + # The media object contains all the details needed to play the item. + + # In 99% of the cases this will be called by the Queue controller to play + # a single item from the queue on the player and the uri within the media + # object will then contain the URL to play that single queue item. + + # If your player provider does not support enqueuing of items, + # the queue controller will simply call this play_media method for + # each item in the queue to play them one by one. + + # In order to support true gapless and/or crossfade, we offer the option of + # 'flow_mode' playback. In that case the queue controller will stitch together + # all songs in the playback queue into a single stream and send that to the player. + # In that case the URI (and metadata) received here is that of the 'flow mode' stream. + + # Examples of player providers that use flow mode for playback by default are Airplay, + # SnapCast and Fully Kiosk. + + # Examples of player providers that optionally use 'flow mode' are Google Cast and + # Home Assistant. They provide a config entry to enable flow mode playback. + + # Examples of player providers that natively support enqueuing of items are Sonos, + # Slimproto and Google Cast. + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """ + Handle enqueuing of the next (queue) item on the player. + + Called when player reports it started buffering a queue item + and when the queue items updated. + + A PlayerProvider implementation is in itself responsible for handling this + so that the queue items keep playing until its empty or the player stopped. + + This will NOT be called if the end of the queue is reached (and repeat disabled). + This will NOT be called if the player is using flow mode to playback the queue. + """ + # this method should handle the enqueuing of the next queue item on the player. + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup master or group player. + """ + # OPTIONAL - required only if you specified ProviderFeature.SYNC_PLAYERS + # this method should handle the sync command for the given player. + # you should join the given player to the target_player/syncgroup. + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + sonos_player = self.sonos_players[player_id] + await sonos_player.client.player.leave_group() + + async def play_announcement( + self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None + ) -> None: + """Handle (provider native) playback of an announcement on given player.""" + # OPTIONAL - required only if you specified PlayerFeature.PLAY_ANNOUNCEMENT + # This method should handle the playback of an announcement on the given player. + # The announcement object contains all the details needed to play the announcement. + # The volume_level is optional and can be used to set the volume level for the announcement. + # If you do not use the announcement playerfeature, the default behavior is to play the + # announcement as a regular media item using the play_media method and the MA player manager + # will take care of setting the volume level for the announcement and resuming etc. + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + # OPTIONAL + # This method is optional and should be implemented if you specified 'needs_poll' + # on the Player object. This method should poll the player for state changes + # and update the player object in the player manager if needed. + # This method will be called at the interval specified in the poll_interval attribute. diff --git a/music_assistant/providers/_template_player_provider/icon.svg b/music_assistant/providers/_template_player_provider/icon.svg new file mode 100644 index 00000000..845920ca --- /dev/null +++ b/music_assistant/providers/_template_player_provider/icon.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 25 25" version="1.1"> +<g id="surface1"> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 1.5 0 L 23.5 0 C 24.328125 0 25 0.671875 25 1.5 L 25 23.5 C 25 24.328125 24.328125 25 23.5 25 L 1.5 25 C 0.671875 25 0 24.328125 0 23.5 L 0 1.5 C 0 0.671875 0.671875 0 1.5 0 Z M 1.5 0 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 10.386719 18.875 L 14.8125 7.125 L 16.113281 7.125 L 11.6875 18.875 Z M 10.386719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 21.371094 18.875 L 16.945312 7.125 L 18.246094 7.125 L 22.671875 18.875 Z M 21.371094 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.636719 18.875 L 2.636719 7.125 L 3.875 7.125 L 3.875 18.875 Z M 2.636719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 5.445312 18.875 L 5.445312 7.125 L 6.683594 7.125 L 6.683594 18.875 Z M 5.445312 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 8.253906 18.875 L 8.253906 7.125 L 9.492188 7.125 L 9.492188 18.875 Z M 8.253906 18.875 "/> +</g> +</svg> diff --git a/music_assistant/providers/_template_player_provider/manifest.json b/music_assistant/providers/_template_player_provider/manifest.json new file mode 100644 index 00000000..15d6b83a --- /dev/null +++ b/music_assistant/providers/_template_player_provider/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "template_player_provider", + "name": "Name of the Player provider goes here", + "description": "Short description of the player provider goes here", + "codeowners": ["@yourgithubusername"], + "requirements": [], + "documentation": "Link to the documentation on the music-assistant.io helppage (may be added later).", + "mdns_discovery": ["_optional_mdns_service_type._tcp.local."] +} diff --git a/music_assistant/providers/airplay/__init__.py b/music_assistant/providers/airplay/__init__.py new file mode 100644 index 00000000..83081ea5 --- /dev/null +++ b/music_assistant/providers/airplay/__init__.py @@ -0,0 +1,54 @@ +"""Airplay Player provider for Music Assistant.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.provider import ProviderManifest + +from music_assistant import MusicAssistant + +from .const import CONF_BIND_INTERFACE +from .provider import AirplayProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_BIND_INTERFACE, + type=ConfigEntryType.STRING, + default_value=mass.streams.publish_ip, + label="Bind interface", + description="Interface to bind to for Airplay streaming.", + category="advanced", + ), + ) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return AirplayProvider(mass, manifest, config) diff --git a/music_assistant/providers/airplay/bin/cliraop-linux-aarch64 b/music_assistant/providers/airplay/bin/cliraop-linux-aarch64 new file mode 100755 index 00000000..21410d3f Binary files /dev/null and b/music_assistant/providers/airplay/bin/cliraop-linux-aarch64 differ diff --git a/music_assistant/providers/airplay/bin/cliraop-linux-x86_64 b/music_assistant/providers/airplay/bin/cliraop-linux-x86_64 new file mode 100755 index 00000000..95424e6f Binary files /dev/null and b/music_assistant/providers/airplay/bin/cliraop-linux-x86_64 differ diff --git a/music_assistant/providers/airplay/bin/cliraop-macos-arm64 b/music_assistant/providers/airplay/bin/cliraop-macos-arm64 new file mode 100755 index 00000000..0424e653 Binary files /dev/null and b/music_assistant/providers/airplay/bin/cliraop-macos-arm64 differ diff --git a/music_assistant/providers/airplay/const.py b/music_assistant/providers/airplay/const.py new file mode 100644 index 00000000..ea106adc --- /dev/null +++ b/music_assistant/providers/airplay/const.py @@ -0,0 +1,31 @@ +"""Constants for the AirPlay provider.""" + +from __future__ import annotations + +from music_assistant_models.enums import ContentType +from music_assistant_models.media_items import AudioFormat + +DOMAIN = "airplay" + +CONF_ENCRYPTION = "encryption" +CONF_ALAC_ENCODE = "alac_encode" +CONF_VOLUME_START = "volume_start" +CONF_PASSWORD = "password" +CONF_BIND_INTERFACE = "bind_interface" +CONF_READ_AHEAD_BUFFER = "read_ahead_buffer" + +BACKOFF_TIME_LOWER_LIMIT = 15 # seconds +BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes + +CONF_CREDENTIALS = "credentials" +CACHE_KEY_PREV_VOLUME = "airplay_prev_volume" +FALLBACK_VOLUME = 20 + +AIRPLAY_FLOW_PCM_FORMAT = AudioFormat( + content_type=ContentType.PCM_F32LE, + sample_rate=44100, + bit_depth=32, +) +AIRPLAY_PCM_FORMAT = AudioFormat( + content_type=ContentType.from_bit_depth(16), sample_rate=44100, bit_depth=16 +) diff --git a/music_assistant/providers/airplay/helpers.py b/music_assistant/providers/airplay/helpers.py new file mode 100644 index 00000000..fe8f5180 --- /dev/null +++ b/music_assistant/providers/airplay/helpers.py @@ -0,0 +1,52 @@ +"""Various helpers/utilities for the Airplay provider.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zeroconf import IPVersion + +if TYPE_CHECKING: + from zeroconf.asyncio import AsyncServiceInfo + + +def convert_airplay_volume(value: float) -> int: + """Remap Airplay Volume to 0..100 scale.""" + airplay_min = -30 + airplay_max = 0 + normal_min = 0 + normal_max = 100 + portion = (value - airplay_min) * (normal_max - normal_min) / (airplay_max - airplay_min) + return int(portion + normal_min) + + +def get_model_from_am(am_property: str | None) -> tuple[str, str]: + """Return Manufacturer and Model name from mdns AM property.""" + manufacturer = "Unknown" + model = "Generic Airplay device" + if not am_property: + return (manufacturer, model) + if isinstance(am_property, bytes): + am_property = am_property.decode("utf-8") + if am_property == "AudioAccessory5,1": + model = "HomePod" + manufacturer = "Apple" + elif "AppleTV" in am_property: + model = "Apple TV" + manufacturer = "Apple" + else: + model = am_property + return (manufacturer, model) + + +def get_primary_ip_address(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + for address in discovery_info.parsed_addresses(IPVersion.V4Only): + if address.startswith("127"): + # filter out loopback address + continue + if address.startswith("169.254"): + # filter out APIPA address + continue + return address + return None diff --git a/music_assistant/providers/airplay/manifest.json b/music_assistant/providers/airplay/manifest.json new file mode 100644 index 00000000..3dbbbbb6 --- /dev/null +++ b/music_assistant/providers/airplay/manifest.json @@ -0,0 +1,17 @@ +{ + "type": "player", + "domain": "airplay", + "name": "Airplay", + "description": "Support for players that support the Airplay protocol.", + "codeowners": [ + "@music-assistant" + ], + "requirements": [], + "documentation": "https://music-assistant.io/player-support/airplay/", + "multi_instance": false, + "builtin": false, + "icon": "cast-variant", + "mdns_discovery": [ + "_raop._tcp.local." + ] +} diff --git a/music_assistant/providers/airplay/player.py b/music_assistant/providers/airplay/player.py new file mode 100644 index 00000000..8eed6323 --- /dev/null +++ b/music_assistant/providers/airplay/player.py @@ -0,0 +1,50 @@ +"""AirPlay Player definition.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.enums import PlayerState + +if TYPE_CHECKING: + from zeroconf.asyncio import AsyncServiceInfo + + from .provider import AirplayProvider + from .raop import RaopStream + + +class AirPlayPlayer: + """Holds the details of the (discovered) Airplay (RAOP) player.""" + + def __init__( + self, prov: AirplayProvider, player_id: str, discovery_info: AsyncServiceInfo, address: str + ) -> None: + """Initialize AirPlayPlayer.""" + self.prov = prov + self.mass = prov.mass + self.player_id = player_id + self.discovery_info = discovery_info + self.address = address + self.logger = prov.logger.getChild(player_id) + self.raop_stream: RaopStream | None = None + self.last_command_sent = 0.0 + + async def cmd_stop(self, update_state: bool = True) -> None: + """Send STOP command to player.""" + if self.raop_stream: + # forward stop to the entire stream session + await self.raop_stream.session.stop() + if update_state and (mass_player := self.mass.players.get(self.player_id)): + mass_player.state = PlayerState.IDLE + self.mass.players.update(mass_player.player_id) + + async def cmd_play(self) -> None: + """Send PLAY (unpause) command to player.""" + if self.raop_stream and self.raop_stream.running: + await self.raop_stream.send_cli_command("ACTION=PLAY") + + async def cmd_pause(self) -> None: + """Send PAUSE command to player.""" + if not self.raop_stream or not self.raop_stream.running: + return + await self.raop_stream.send_cli_command("ACTION=PAUSE") diff --git a/music_assistant/providers/airplay/provider.py b/music_assistant/providers/airplay/provider.py new file mode 100644 index 00000000..34904afe --- /dev/null +++ b/music_assistant/providers/airplay/provider.py @@ -0,0 +1,640 @@ +"""Airplay Player provider for Music Assistant.""" + +from __future__ import annotations + +import asyncio +import os +import platform +import socket +import time +from random import randrange +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_EQ_BASS, + CONF_ENTRY_EQ_MID, + CONF_ENTRY_EQ_TREBLE, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_OUTPUT_CHANNELS, + CONF_ENTRY_SYNC_ADJUST, + ConfigEntry, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + MediaType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderFeature, +) +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from zeroconf import ServiceStateChange +from zeroconf.asyncio import AsyncServiceInfo + +from music_assistant.helpers import ( + convert_airplay_volume, + get_model_from_am, + get_primary_ip_address, +) +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.datetime import utc +from music_assistant.helpers.process import check_output +from music_assistant.helpers.util import TaskManager, get_ip_pton, lock, select_free_port +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.airplay.raop import RaopStreamSession + +from .const import ( + AIRPLAY_FLOW_PCM_FORMAT, + AIRPLAY_PCM_FORMAT, + CACHE_KEY_PREV_VOLUME, + CONF_ALAC_ENCODE, + CONF_ENCRYPTION, + CONF_PASSWORD, + CONF_READ_AHEAD_BUFFER, + FALLBACK_VOLUME, +) +from .player import AirPlayPlayer + +if TYPE_CHECKING: + from music_assistant.providers.player_group import PlayerGroupProvider + + +PLAYER_CONFIG_ENTRIES = ( + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_EQ_BASS, + CONF_ENTRY_EQ_MID, + CONF_ENTRY_EQ_TREBLE, + CONF_ENTRY_OUTPUT_CHANNELS, + ConfigEntry( + key=CONF_ENCRYPTION, + type=ConfigEntryType.BOOLEAN, + default_value=False, + label="Enable encryption", + description="Enable encrypted communication with the player, " + "some (3rd party) players require this.", + category="airplay", + ), + ConfigEntry( + key=CONF_ALAC_ENCODE, + type=ConfigEntryType.BOOLEAN, + default_value=True, + label="Enable compression", + description="Save some network bandwidth by sending the audio as " + "(lossless) ALAC at the cost of a bit CPU.", + category="airplay", + ), + CONF_ENTRY_SYNC_ADJUST, + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + default_value=None, + required=False, + label="Device password", + description="Some devices require a password to connect/play.", + category="airplay", + ), + ConfigEntry( + key=CONF_READ_AHEAD_BUFFER, + type=ConfigEntryType.INTEGER, + default_value=1000, + required=False, + label="Audio buffer (ms)", + description="Amount of buffer (in milliseconds), " + "the player should keep to absorb network throughput jitter. " + "If you experience audio dropouts, try increasing this value.", + category="airplay", + range=(500, 3000), + ), + # airplay has fixed sample rate/bit depth so make this config entry static and hidden + create_sample_rates_config_entry(44100, 16, 44100, 16, True), +) + + +# TODO: Airplay provider +# - Implement authentication for Apple TV +# - Implement volume control for Apple devices using pyatv +# - Implement metadata for Apple Apple devices using pyatv +# - Use pyatv for communicating with original Apple devices (and use cliraop for actual streaming) +# - Implement Airplay 2 support +# - Implement late joining to existing stream (instead of restarting it) + + +class AirplayProvider(PlayerProvider): + """Player provider for Airplay based players.""" + + cliraop_bin: str | None = None + _players: dict[str, AirPlayPlayer] + _dacp_server: asyncio.Server = None + _dacp_info: AsyncServiceInfo = None + _play_media_lock: asyncio.Lock = asyncio.Lock() + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self._players = {} + self.cliraop_bin = await self._getcliraop_binary() + dacp_port = await select_free_port(39831, 49831) + self.dacp_id = dacp_id = f"{randrange(2 ** 64):X}" + self.logger.debug("Starting DACP ActiveRemote %s on port %s", dacp_id, dacp_port) + self._dacp_server = await asyncio.start_server( + self._handle_dacp_request, "0.0.0.0", dacp_port + ) + zeroconf_type = "_dacp._tcp.local." + server_id = f"iTunes_Ctrl_{dacp_id}.{zeroconf_type}" + self._dacp_info = AsyncServiceInfo( + zeroconf_type, + name=server_id, + addresses=[await get_ip_pton(self.mass.streams.publish_ip)], + port=dacp_port, + properties={ + "txtvers": "1", + "Ver": "63B5E5C0C201542E", + "DbId": "63B5E5C0C201542E", + "OSsi": "0x1F5", + }, + server=f"{socket.gethostname()}.local", + ) + await self.mass.aiozc.async_register_service(self._dacp_info) + + async def on_mdns_service_state_change( + self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None + ) -> None: + """Handle MDNS service state callback.""" + raw_id, display_name = name.split(".")[0].split("@", 1) + player_id = f"ap{raw_id.lower()}" + # handle removed player + if state_change == ServiceStateChange.Removed: + if mass_player := self.mass.players.get(player_id): + if not mass_player.available: + return + # the player has become unavailable + self.logger.debug("Player offline: %s", display_name) + mass_player.available = False + self.mass.players.update(player_id) + return + # handle update for existing device + if airplay_player := self._players.get(player_id): + if mass_player := self.mass.players.get(player_id): + cur_address = get_primary_ip_address(info) + if cur_address and cur_address != airplay_player.address: + airplay_player.logger.debug( + "Address updated from %s to %s", airplay_player.address, cur_address + ) + airplay_player.address = cur_address + mass_player.device_info = DeviceInfo( + model=mass_player.device_info.model, + manufacturer=mass_player.device_info.manufacturer, + address=str(cur_address), + ) + if not mass_player.available: + self.logger.debug("Player back online: %s", display_name) + mass_player.available = True + # always update the latest discovery info + airplay_player.discovery_info = info + self.mass.players.update(player_id) + return + # handle new player + await self._setup_player(player_id, display_name, info) + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + # power off all players (will disconnect and close cliraop) + for player_id in self._players: + await self.cmd_power(player_id, False) + # shutdown DACP server + if self._dacp_server: + self._dacp_server.close() + # shutdown DACP zeroconf service + if self._dacp_info: + await self.mass.aiozc.async_unregister_service(self._dacp_info) + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + base_entries = await super().get_player_config_entries(player_id) + return (*base_entries, *PLAYER_CONFIG_ENTRIES) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player. + + - player_id: player_id of the player to handle the command. + """ + if airplay_player := self._players.get(player_id): + await airplay_player.cmd_stop() + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY (unpause) command to given player. + + - player_id: player_id of the player to handle the command. + """ + if airplay_player := self._players.get(player_id): + await airplay_player.cmd_play() + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player. + + - player_id: player_id of the player to handle the command. + """ + player = self.mass.players.get(player_id) + if player.group_childs: + # pause is not supported while synced, use stop instead + self.logger.debug("Player is synced, using STOP instead of PAUSE") + await self.cmd_stop(player_id) + return + airplay_player = self._players[player_id] + await airplay_player.cmd_pause() + + @lock + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + async with self._play_media_lock: + player = self.mass.players.get(player_id) + # set the active source for the player to the media queue + # this accounts for syncgroups and linked players (e.g. sonos) + player.active_source = media.queue_id + if player.synced_to: + # should not happen, but just in case + raise RuntimeError("Player is synced") + # always stop existing stream first + async with TaskManager(self.mass) as tg: + for airplay_player in self._get_sync_clients(player_id): + tg.create_task(airplay_player.cmd_stop(update_state=False)) + # select audio source + if media.media_type == MediaType.ANNOUNCEMENT: + # special case: stream announcement + input_format = AIRPLAY_PCM_FORMAT + audio_source = self.mass.streams.get_announcement_stream( + media.custom_data["url"], + output_format=AIRPLAY_PCM_FORMAT, + use_pre_announce=media.custom_data["use_pre_announce"], + ) + elif media.queue_id.startswith("ugp_"): + # special case: UGP stream + ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") + ugp_stream = ugp_provider.ugp_streams[media.queue_id] + input_format = ugp_stream.output_format + audio_source = ugp_stream.subscribe() + elif media.queue_id and media.queue_item_id: + # regular queue (flow) stream request + input_format = AIRPLAY_FLOW_PCM_FORMAT + audio_source = self.mass.streams.get_flow_stream( + queue=self.mass.player_queues.get(media.queue_id), + start_queue_item=self.mass.player_queues.get_item( + media.queue_id, media.queue_item_id + ), + pcm_format=input_format, + ) + else: + # assume url or some other direct path + # NOTE: this will fail if its an uri not playable by ffmpeg + input_format = AIRPLAY_PCM_FORMAT + audio_source = get_ffmpeg_stream( + audio_input=media.uri, + input_format=AudioFormat(ContentType.try_parse(media.uri)), + output_format=AIRPLAY_PCM_FORMAT, + ) + # setup RaopStreamSession for player (and its sync childs if any) + sync_clients = self._get_sync_clients(player_id) + raop_stream_session = RaopStreamSession(self, sync_clients, input_format, audio_source) + await raop_stream_session.start() + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player. + + - player_id: player_id of the player to handle the command. + - volume_level: volume level (0..100) to set on the player. + """ + airplay_player = self._players[player_id] + if airplay_player.raop_stream and airplay_player.raop_stream.running: + await airplay_player.raop_stream.send_cli_command(f"VOLUME={volume_level}\n") + mass_player = self.mass.players.get(player_id) + mass_player.volume_level = volume_level + mass_player.volume_muted = volume_level == 0 + self.mass.players.update(player_id) + # store last state in cache + await self.mass.cache.set(player_id, volume_level, base_key=CACHE_KEY_PREV_VOLUME) + + @lock + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup master or group player. + """ + if player_id == target_player: + return + child_player = self.mass.players.get(player_id) + assert child_player # guard + parent_player = self.mass.players.get(target_player) + assert parent_player # guard + if parent_player.synced_to: + raise RuntimeError("Player is already synced") + if child_player.synced_to and child_player.synced_to != target_player: + raise RuntimeError("Player is already synced to another player") + if player_id in parent_player.group_childs: + # nothing to do: player is already part of the group + return + # ensure the child does not have an existing steam session active + if airplay_player := self._players.get(player_id): + if airplay_player.raop_stream and airplay_player.raop_stream.running: + await airplay_player.raop_stream.session.remove_client(airplay_player) + # always make sure that the parent player is part of the sync group + parent_player.group_childs.add(parent_player.player_id) + parent_player.group_childs.add(child_player.player_id) + child_player.synced_to = parent_player.player_id + # mark players as powered + parent_player.powered = True + child_player.powered = True + # check if we should (re)start or join a stream session + active_queue = self.mass.player_queues.get_active_queue(parent_player.player_id) + if active_queue.state == PlayerState.PLAYING: + # playback needs to be restarted to form a new multi client stream session + # this could potentially be called by multiple players at the exact same time + # so we debounce the resync a bit here with a timer + self.mass.call_later( + 1, + self.mass.player_queues.resume, + active_queue.queue_id, + fade_in=False, + task_id=f"resume_{active_queue.queue_id}", + ) + else: + # make sure that the player manager gets an update + self.mass.players.update(child_player.player_id, skip_forward=True) + self.mass.players.update(parent_player.player_id, skip_forward=True) + + @lock + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + mass_player = self.mass.players.get(player_id, raise_unavailable=True) + if not mass_player.synced_to: + return + ap_player = self._players[player_id] + if ap_player.raop_stream and ap_player.raop_stream.running: + await ap_player.raop_stream.session.remove_client(ap_player) + group_leader = self.mass.players.get(mass_player.synced_to, raise_unavailable=True) + if player_id in group_leader.group_childs: + group_leader.group_childs.remove(player_id) + mass_player.synced_to = None + airplay_player = self._players.get(player_id) + await airplay_player.cmd_stop() + # make sure that the player manager gets an update + self.mass.players.update(mass_player.player_id, skip_forward=True) + self.mass.players.update(group_leader.player_id, skip_forward=True) + + async def _getcliraop_binary(self): + """Find the correct raop/airplay binary belonging to the platform.""" + # ruff: noqa: SIM102 + if self.cliraop_bin is not None: + return self.cliraop_bin + + async def check_binary(cliraop_path: str) -> str | None: + try: + returncode, output = await check_output( + cliraop_path, + "-check", + ) + if returncode == 0 and output.strip().decode() == "cliraop check": + self.cliraop_bin = cliraop_path + return cliraop_path + except OSError: + return None + + base_path = os.path.join(os.path.dirname(__file__), "bin") + system = platform.system().lower().replace("darwin", "macos") + architecture = platform.machine().lower() + + if bridge_binary := await check_binary( + os.path.join(base_path, f"cliraop-{system}-{architecture}") + ): + return bridge_binary + + msg = f"Unable to locate RAOP Play binary for {system}/{architecture}" + raise RuntimeError(msg) + + def _get_sync_clients(self, player_id: str) -> list[AirPlayPlayer]: + """Get all sync clients for a player.""" + mass_player = self.mass.players.get(player_id, True) + sync_clients: list[AirPlayPlayer] = [] + # we need to return the player itself too + group_child_ids = {player_id} + group_child_ids.update(mass_player.group_childs) + for child_id in group_child_ids: + if client := self._players.get(child_id): + sync_clients.append(client) + return sync_clients + + async def _setup_player( + self, player_id: str, display_name: str, info: AsyncServiceInfo + ) -> None: + """Handle setup of a new player that is discovered using mdns.""" + address = get_primary_ip_address(info) + if address is None: + return + self.logger.debug("Discovered Airplay device %s on %s", display_name, address) + manufacturer, model = get_model_from_am(info.decoded_properties.get("am")) + if "apple tv" in model.lower(): + # For now, we ignore the Apple TV until we implement the authentication. + # maybe we can simply use pyatv only for this part? + # the cliraop application has already been prepared to accept the secret. + self.logger.debug( + "Ignoring %s in discovery due to authentication requirement.", display_name + ) + return + if not self.mass.config.get_raw_player_config_value(player_id, "enabled", True): + self.logger.debug("Ignoring %s in discovery as it is disabled.", display_name) + return + self._players[player_id] = AirPlayPlayer(self, player_id, info, address) + if not (volume := await self.mass.cache.get(player_id, base_key=CACHE_KEY_PREV_VOLUME)): + volume = FALLBACK_VOLUME + mass_player = Player( + player_id=player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=display_name, + available=True, + powered=False, + device_info=DeviceInfo( + model=model, + manufacturer=manufacturer, + address=address, + ), + supported_features=( + PlayerFeature.PAUSE, + PlayerFeature.SYNC, + PlayerFeature.VOLUME_SET, + ), + volume_level=volume, + ) + await self.mass.players.register_or_update(mass_player) + + async def _handle_dacp_request( # noqa: PLR0915 + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + """Handle new connection on the socket.""" + try: + raw_request = b"" + while recv := await reader.read(1024): + raw_request += recv + if len(recv) < 1024: + break + + request = raw_request.decode("UTF-8") + if "\r\n\r\n" in request: + headers_raw, body = request.split("\r\n\r\n", 1) + else: + headers_raw = request + body = "" + headers_raw = headers_raw.split("\r\n") + headers = {} + for line in headers_raw[1:]: + if ":" not in line: + continue + x, y = line.split(":", 1) + headers[x.strip()] = y.strip() + active_remote = headers.get("Active-Remote") + _, path, _ = headers_raw[0].split(" ") + airplay_player = next( + ( + x + for x in self._players.values() + if x.raop_stream and x.raop_stream.active_remote_id == active_remote + ), + None, + ) + self.logger.debug( + "DACP request for %s (%s): %s -- %s", + airplay_player.discovery_info.name if airplay_player else "UNKNOWN PLAYER", + active_remote, + path, + body, + ) + if not airplay_player: + return + + player_id = airplay_player.player_id + mass_player = self.mass.players.get(player_id) + active_queue = self.mass.player_queues.get_active_queue(player_id) + if path == "/ctrl-int/1/nextitem": + self.mass.create_task(self.mass.player_queues.next(active_queue.queue_id)) + elif path == "/ctrl-int/1/previtem": + self.mass.create_task(self.mass.player_queues.previous(active_queue.queue_id)) + elif path == "/ctrl-int/1/play": + # sometimes this request is sent by a device as confirmation of a play command + # we ignore this if the player is already playing + if mass_player.state != PlayerState.PLAYING: + self.mass.create_task(self.mass.player_queues.play(active_queue.queue_id)) + elif path == "/ctrl-int/1/playpause": + self.mass.create_task(self.mass.player_queues.play_pause(active_queue.queue_id)) + elif path == "/ctrl-int/1/stop": + self.mass.create_task(self.mass.player_queues.stop(active_queue.queue_id)) + elif path == "/ctrl-int/1/volumeup": + self.mass.create_task(self.mass.players.cmd_volume_up(player_id)) + elif path == "/ctrl-int/1/volumedown": + self.mass.create_task(self.mass.players.cmd_volume_down(player_id)) + elif path == "/ctrl-int/1/shuffle_songs": + queue = self.mass.player_queues.get(player_id) + self.mass.loop.call_soon( + self.mass.player_queues.set_shuffle( + active_queue.queue_id, not queue.shuffle_enabled + ) + ) + elif path in ("/ctrl-int/1/pause", "/ctrl-int/1/discrete-pause"): + # sometimes this request is sent by a device as confirmation of a play command + # we ignore this if the player is already playing + if mass_player.state == PlayerState.PLAYING: + self.mass.create_task(self.mass.player_queues.pause(active_queue.queue_id)) + elif "dmcp.device-volume=" in path: + if mass_player.device_info.manufacturer.lower() == "apple": + # Apple devices only report their previous volume level ?! + return + # This is a bit annoying as this can be either the device confirming a new volume + # we've sent or the device requesting a new volume itself. + # In case of a small rounding difference, we ignore this, + # to prevent an endless pingpong of volume changes + raop_volume = float(path.split("dmcp.device-volume=", 1)[-1]) + volume = convert_airplay_volume(raop_volume) + if ( + abs(mass_player.volume_level - volume) > 5 + or (time.time() - airplay_player.last_command_sent) < 2 + ): + self.mass.create_task(self.cmd_volume_set(player_id, volume)) + else: + mass_player.volume_level = volume + self.mass.players.update(player_id) + elif "dmcp.volume=" in path: + # volume change request from device (e.g. volume buttons) + volume = int(path.split("dmcp.volume=", 1)[-1]) + if volume != mass_player.volume_level: + self.mass.create_task(self.cmd_volume_set(player_id, volume)) + # optimistically set the new volume to prevent bouncing around + mass_player.volume_level = volume + elif "device-prevent-playback=1" in path: + # device switched to another source (or is powered off) + if raop_stream := airplay_player.raop_stream: + # ignore this if we just started playing to prevent false positives + if mass_player.elapsed_time > 10 and mass_player.state == PlayerState.PLAYING: + raop_stream.prevent_playback = True + self.mass.create_task(self.monitor_prevent_playback(player_id)) + elif "device-prevent-playback=0" in path: + # device reports that its ready for playback again + if raop_stream := airplay_player.raop_stream: + raop_stream.prevent_playback = False + + # send response + date_str = utc().strftime("%a, %-d %b %Y %H:%M:%S") + response = ( + f"HTTP/1.0 204 No Content\r\nDate: {date_str} " + "GMT\r\nDAAP-Server: iTunes/7.6.2 (Windows; N;)\r\nContent-Type: " + "application/x-dmap-tagged\r\nContent-Length: 0\r\n" + "Connection: close\r\n\r\n" + ) + writer.write(response.encode()) + await writer.drain() + finally: + writer.close() + + async def monitor_prevent_playback(self, player_id: str): + """Monitor the prevent playback state of an airplay player.""" + count = 0 + if not (airplay_player := self._players.get(player_id)): + return + prev_active_remote_id = airplay_player.raop_stream.active_remote_id + while count < 40: + count += 1 + if not (airplay_player := self._players.get(player_id)): + return + if not (raop_stream := airplay_player.raop_stream): + return + if raop_stream.active_remote_id != prev_active_remote_id: + # checksum + return + if not raop_stream.prevent_playback: + return + await asyncio.sleep(0.5) + + airplay_player.logger.info( + "Player has been in prevent playback mode for too long, powering off.", + ) + await self.mass.players.cmd_power(airplay_player.player_id, False) diff --git a/music_assistant/providers/airplay/raop.py b/music_assistant/providers/airplay/raop.py new file mode 100644 index 00000000..e1b74942 --- /dev/null +++ b/music_assistant/providers/airplay/raop.py @@ -0,0 +1,401 @@ +"""Logic for RAOP (AirPlay 1) audio streaming to Airplay devices.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import platform +import time +from collections.abc import AsyncGenerator +from contextlib import suppress +from random import randint +from typing import TYPE_CHECKING + +from music_assistant_models.enums import PlayerState + +from music_assistant.constants import CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL +from music_assistant.helpers.audio import get_player_filter_params +from music_assistant.helpers.ffmpeg import FFMpeg +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.util import close_async_generator + +from .const import ( + AIRPLAY_PCM_FORMAT, + CONF_ALAC_ENCODE, + CONF_BIND_INTERFACE, + CONF_ENCRYPTION, + CONF_PASSWORD, + CONF_READ_AHEAD_BUFFER, +) + +if TYPE_CHECKING: + from music_assistant_models.media_items import AudioFormat + from music_assistant_models.player_queue import PlayerQueue + + from .player import AirPlayPlayer + from .provider import AirplayProvider + + +class RaopStreamSession: + """Object that holds the details of a (RAOP) stream session to one or more players.""" + + def __init__( + self, + airplay_provider: AirplayProvider, + sync_clients: list[AirPlayPlayer], + input_format: AudioFormat, + audio_source: AsyncGenerator[bytes, None], + ) -> None: + """Initialize RaopStreamSession.""" + assert sync_clients + self.prov = airplay_provider + self.mass = airplay_provider.mass + self.input_format = input_format + self._sync_clients = sync_clients + self._audio_source = audio_source + self._audio_source_task: asyncio.Task | None = None + self._stopped: bool = False + self._lock = asyncio.Lock() + + async def start(self) -> None: + """Initialize RaopStreamSession.""" + # initialize raop stream for all players + for airplay_player in self._sync_clients: + if airplay_player.raop_stream and airplay_player.raop_stream.running: + raise RuntimeError("Player already has an active stream") + airplay_player.raop_stream = RaopStream(self, airplay_player) + + async def audio_streamer() -> None: + """Stream audio to all players.""" + generator_exhausted = False + try: + async for chunk in self._audio_source: + if not self._sync_clients: + return + async with self._lock: + await asyncio.gather( + *[x.raop_stream.write_chunk(chunk) for x in self._sync_clients], + return_exceptions=True, + ) + # entire stream consumed: send EOF + generator_exhausted = True + async with self._lock: + await asyncio.gather( + *[x.raop_stream.write_eof() for x in self._sync_clients], + return_exceptions=True, + ) + finally: + if not generator_exhausted: + await close_async_generator(self._audio_source) + + # get current ntp and start RaopStream per player + _, stdout = await check_output(self.prov.cliraop_bin, "-ntp") + start_ntp = int(stdout.strip()) + wait_start = 1500 + (250 * len(self._sync_clients)) + async with self._lock: + await asyncio.gather( + *[x.raop_stream.start(start_ntp, wait_start) for x in self._sync_clients], + return_exceptions=True, + ) + self._audio_source_task = asyncio.create_task(audio_streamer()) + + async def stop(self) -> None: + """Stop playback and cleanup.""" + if self._stopped: + return + self._stopped = True + if self._audio_source_task and not self._audio_source_task.done(): + self._audio_source_task.cancel() + await asyncio.gather( + *[self.remove_client(x) for x in self._sync_clients], + return_exceptions=True, + ) + + async def remove_client(self, airplay_player: AirPlayPlayer) -> None: + """Remove a sync client from the session.""" + if airplay_player not in self._sync_clients: + return + assert airplay_player.raop_stream.session == self + async with self._lock: + self._sync_clients.remove(airplay_player) + await airplay_player.raop_stream.stop() + airplay_player.raop_stream = None + + async def add_client(self, airplay_player: AirPlayPlayer) -> None: + """Add a sync client to the session.""" + # TODO: Add the ability to add a new client to an existing session + # e.g. by counting the number of frames sent etc. + raise NotImplementedError("Adding clients to a session is not yet supported") + + +class RaopStream: + """ + RAOP (Airplay 1) Audio Streamer. + + Python is not suitable for realtime audio streaming so we do the actual streaming + of (RAOP) audio using a small executable written in C based on libraop to do + the actual timestamped playback, which reads pcm audio from stdin + and we can send some interactive commands using a named pipe. + """ + + def __init__( + self, + session: RaopStreamSession, + airplay_player: AirPlayPlayer, + ) -> None: + """Initialize RaopStream.""" + self.session = session + self.prov = session.prov + self.mass = session.prov.mass + self.airplay_player = airplay_player + + # always generate a new active remote id to prevent race conditions + # with the named pipe used to send audio + self.active_remote_id: str = str(randint(1000, 8000)) + self.prevent_playback: bool = False + self._log_reader_task: asyncio.Task | None = None + self._cliraop_proc: AsyncProcess | None = None + self._ffmpeg_proc: AsyncProcess | None = None + self._started = asyncio.Event() + self._stopped = False + + @property + def running(self) -> bool: + """Return boolean if this stream is running.""" + return not self._stopped and self._started.is_set() + + async def start(self, start_ntp: int, wait_start: int = 1000) -> None: + """Initialize CLIRaop process for a player.""" + extra_args = [] + player_id = self.airplay_player.player_id + mass_player = self.mass.players.get(player_id) + bind_ip = await self.mass.config.get_provider_config_value( + self.prov.instance_id, CONF_BIND_INTERFACE + ) + extra_args += ["-if", bind_ip] + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENCRYPTION, False): + extra_args += ["-encrypt"] + if self.mass.config.get_raw_player_config_value(player_id, CONF_ALAC_ENCODE, True): + extra_args += ["-alac"] + for prop in ("et", "md", "am", "pk", "pw"): + if prop_value := self.airplay_player.discovery_info.decoded_properties.get(prop): + extra_args += [f"-{prop}", prop_value] + sync_adjust = self.mass.config.get_raw_player_config_value(player_id, CONF_SYNC_ADJUST, 0) + if device_password := self.mass.config.get_raw_player_config_value( + player_id, CONF_PASSWORD, None + ): + extra_args += ["-password", device_password] + if self.prov.logger.isEnabledFor(logging.DEBUG): + extra_args += ["-debug", "5"] + elif self.prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + extra_args += ["-debug", "10"] + read_ahead = await self.mass.config.get_player_config_value( + player_id, CONF_READ_AHEAD_BUFFER + ) + + # create os pipes to pipe ffmpeg to cliraop + read, write = await asyncio.to_thread(os.pipe) + + # ffmpeg handles the player specific stream + filters and pipes + # audio to the cliraop process + self._ffmpeg_proc = FFMpeg( + audio_input="-", + input_format=self.session.input_format, + output_format=AIRPLAY_PCM_FORMAT, + filter_params=get_player_filter_params(self.mass, player_id), + audio_output=write, + ) + await self._ffmpeg_proc.start() + await asyncio.to_thread(os.close, write) + + # cliraop is the binary that handles the actual raop streaming to the player + cliraop_args = [ + self.prov.cliraop_bin, + "-ntpstart", + str(start_ntp), + "-port", + str(self.airplay_player.discovery_info.port), + "-wait", + str(wait_start - sync_adjust), + "-latency", + str(read_ahead), + "-volume", + str(mass_player.volume_level), + *extra_args, + "-dacp", + self.prov.dacp_id, + "-activeremote", + self.active_remote_id, + "-udn", + self.airplay_player.discovery_info.name, + self.airplay_player.address, + "-", + ] + self._cliraop_proc = AsyncProcess(cliraop_args, stdin=read, stderr=True, name="cliraop") + if platform.system() == "Darwin": + os.environ["DYLD_LIBRARY_PATH"] = "/usr/local/lib" + await self._cliraop_proc.start() + await asyncio.to_thread(os.close, read) + self._started.set() + self._log_reader_task = self.mass.create_task(self._log_watcher()) + + async def stop(self): + """Stop playback and cleanup.""" + if self._stopped: + return + if self._cliraop_proc.proc and not self._cliraop_proc.closed: + await self.send_cli_command("ACTION=STOP") + self._stopped = True # set after send_cli command! + if self._cliraop_proc.proc and not self._cliraop_proc.closed: + await self._cliraop_proc.close(True) + if self._ffmpeg_proc and not self._ffmpeg_proc.closed: + await self._ffmpeg_proc.close(True) + self._cliraop_proc = None + self._ffmpeg_proc = None + + async def write_chunk(self, chunk: bytes) -> None: + """Write a (pcm) audio chunk.""" + if self._stopped: + return + await self._started.wait() + await self._ffmpeg_proc.write(chunk) + + async def write_eof(self) -> None: + """Write EOF.""" + if self._stopped: + return + await self._started.wait() + await self._ffmpeg_proc.write_eof() + + async def send_cli_command(self, command: str) -> None: + """Send an interactive command to the running CLIRaop binary.""" + if self._stopped: + return + await self._started.wait() + + if not command.endswith("\n"): + command += "\n" + + def send_data(): + with suppress(BrokenPipeError), open(named_pipe, "w") as f: + f.write(command) + + named_pipe = f"/tmp/raop-{self.active_remote_id}" # noqa: S108 + self.airplay_player.logger.log(VERBOSE_LOG_LEVEL, "sending command %s", command) + self.airplay_player.last_command_sent = time.time() + await asyncio.to_thread(send_data) + + async def _log_watcher(self) -> None: + """Monitor stderr for the running CLIRaop process.""" + airplay_player = self.airplay_player + mass_player = self.mass.players.get(airplay_player.player_id) + queue = self.mass.player_queues.get_active_queue(mass_player.active_source) + logger = airplay_player.logger + lost_packets = 0 + prev_metadata_checksum: str = "" + prev_progress_report: float = 0 + async for line in self._cliraop_proc.iter_stderr(): + if "elapsed milliseconds:" in line: + # this is received more or less every second while playing + millis = int(line.split("elapsed milliseconds: ")[1]) + mass_player.elapsed_time = millis / 1000 + mass_player.elapsed_time_last_updated = time.time() + # send metadata to player(s) if needed + # NOTE: this must all be done in separate tasks to not disturb audio + now = time.time() + if ( + mass_player.elapsed_time > 2 + and queue + and queue.current_item + and queue.current_item.streamdetails + ): + metadata_checksum = ( + queue.current_item.streamdetails.stream_title + or queue.current_item.queue_item_id + ) + if prev_metadata_checksum != metadata_checksum: + prev_metadata_checksum = metadata_checksum + prev_progress_report = now + self.mass.create_task(self._send_metadata(queue)) + # send the progress report every 5 seconds + elif now - prev_progress_report >= 5: + prev_progress_report = now + self.mass.create_task(self._send_progress(queue)) + if "set pause" in line or "Pause at" in line: + mass_player.state = PlayerState.PAUSED + self.mass.players.update(airplay_player.player_id) + if "Restarted at" in line or "restarting w/ pause" in line: + mass_player.state = PlayerState.PLAYING + self.mass.players.update(airplay_player.player_id) + if "restarting w/o pause" in line: + # streaming has started + mass_player.state = PlayerState.PLAYING + mass_player.elapsed_time = 0 + mass_player.elapsed_time_last_updated = time.time() + self.mass.players.update(airplay_player.player_id) + if "lost packet out of backlog" in line: + 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)) + else: + logger.warning("Packet loss detected!") + if "end of stream reached" in line: + logger.debug("End of stream reached") + break + + logger.log(VERBOSE_LOG_LEVEL, line) + + # if we reach this point, the process exited + if airplay_player.raop_stream == self: + mass_player.state = PlayerState.IDLE + self.mass.players.update(airplay_player.player_id) + # ensure we're cleaned up afterwards (this also logs the returncode) + await self.stop() + + async def _send_metadata(self, queue: PlayerQueue) -> None: + """Send metadata to player (and connected sync childs).""" + if not queue or not queue.current_item: + return + duration = min(queue.current_item.duration or 0, 3600) + title = queue.current_item.name + artist = "" + album = "" + if queue.current_item.streamdetails and queue.current_item.streamdetails.stream_title: + # stream title from radio station + stream_title = queue.current_item.streamdetails.stream_title + if " - " in stream_title: + artist, title = stream_title.split(" - ", 1) + else: + title = stream_title + # set album to radio station name + album = queue.current_item.name + elif media_item := queue.current_item.media_item: + title = media_item.name + if artist_str := getattr(media_item, "artist_str", None): + artist = artist_str + if _album := getattr(media_item, "album", None): + album = _album.name + + cmd = f"TITLE={title or 'Music Assistant'}\nARTIST={artist}\nALBUM={album}\n" + cmd += f"DURATION={duration}\nPROGRESS=0\nACTION=SENDMETA\n" + + await self.send_cli_command(cmd) + + # get image + if not queue.current_item.image: + return + + # the image format needs to be 500x500 jpeg for maximum compatibility with players + image_url = self.mass.metadata.get_image_url( + queue.current_item.image, size=500, prefer_proxy=True, image_format="jpeg" + ) + await self.send_cli_command(f"ARTWORK={image_url}\n") + + async def _send_progress(self, queue: PlayerQueue) -> None: + """Send progress report to player (and connected sync childs).""" + if not queue or not queue.current_item: + return + progress = int(queue.corrected_elapsed_time) + await self.send_cli_command(f"PROGRESS={progress}\n") diff --git a/music_assistant/providers/apple_music/__init__.py b/music_assistant/providers/apple_music/__init__.py new file mode 100644 index 00000000..e753449f --- /dev/null +++ b/music_assistant/providers/apple_music/__init__.py @@ -0,0 +1,810 @@ +"""Apple Music musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +import base64 +import json +import os +from typing import TYPE_CHECKING, Any + +import aiofiles +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + AudioFormat, + ContentType, + ImageType, + ItemMapping, + MediaItemImage, + MediaItemType, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails +from pywidevine import PSSH, Cdm, Device, DeviceTypes +from pywidevine.license_protocol_pb2 import WidevinePsshData + +from music_assistant.constants import CONF_PASSWORD +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.playlists import fetch_playlist +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, +) + +DEVELOPER_TOKEN = app_var(8) +WIDEVINE_BASE_PATH = "/usr/local/bin/widevine_cdm" +DECRYPT_CLIENT_ID_FILENAME = "client_id.bin" +DECRYPT_PRIVATE_KEY_FILENAME = "private_key.pem" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return AppleMusicProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Music user token", + required=True, + ), + ) + + +class AppleMusicProvider(MusicProvider): + """Implementation of an Apple Music MusicProvider.""" + + _music_user_token: str | None = None + _storefront: str | None = None + _decrypt_client_id: bytes | None = None + _decrypt_private_key: bytes | None = None + # rate limiter needs to be specified on provider-level, + # so make it an instance attribute + throttler = ThrottlerManager(rate_limit=1, period=2, initial_backoff=15) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self._music_user_token = self.config.get_value(CONF_PASSWORD) + self._storefront = await self._get_user_storefront() + async with aiofiles.open( + os.path.join(WIDEVINE_BASE_PATH, DECRYPT_CLIENT_ID_FILENAME), "rb" + ) as _file: + self._decrypt_client_id = await _file.read() + async with aiofiles.open( + os.path.join(WIDEVINE_BASE_PATH, DECRYPT_PRIVATE_KEY_FILENAME), "rb" + ) as _file: + self._decrypt_private_key = await _file.read() + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def search( + self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + endpoint = f"catalog/{self._storefront}/search" + # Apple music has a limit of 25 items for the search endpoint + limit = min(limit, 25) + searchresult = SearchResults() + searchtypes = [] + if MediaType.ARTIST in media_types: + searchtypes.append("artists") + if MediaType.ALBUM in media_types: + searchtypes.append("albums") + if MediaType.TRACK in media_types: + searchtypes.append("songs") + if MediaType.PLAYLIST in media_types: + searchtypes.append("playlists") + if not searchtypes: + return searchresult + searchtype = ",".join(searchtypes) + search_query = search_query.replace("'", "") + response = await self._get_data(endpoint, term=search_query, types=searchtype, limit=limit) + if "artists" in response["results"]: + searchresult.artists += [ + self._parse_artist(item) for item in response["results"]["artists"]["data"] + ] + if "albums" in response["results"]: + searchresult.albums += [ + self._parse_album(item) for item in response["results"]["albums"]["data"] + ] + if "songs" in response["results"]: + searchresult.tracks += [ + self._parse_track(item) for item in response["results"]["songs"]["data"] + ] + if "playlists" in response["results"]: + searchresult.playlists += [ + self._parse_playlist(item) for item in response["results"]["playlists"]["data"] + ] + return searchresult + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve library artists from spotify.""" + endpoint = "me/library/artists" + for item in await self._get_all_items(endpoint, include="catalog", extend="editorialNotes"): + if item and item["id"]: + yield self._parse_artist(item) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve library albums from the provider.""" + endpoint = "me/library/albums" + for item in await self._get_all_items( + endpoint, include="catalog,artists", extend="editorialNotes" + ): + if item and item["id"]: + album = self._parse_album(item) + if album: + yield album + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + endpoint = "me/library/songs" + song_catalog_ids = [] + for item in await self._get_all_items(endpoint): + catalog_id = item.get("attributes", {}).get("playParams", {}).get("catalogId") + if not catalog_id: + self.logger.debug( + "Skipping track. No catalog version found for %s - %s", + item["attributes"].get("artistName", ""), + item["attributes"].get("name", ""), + ) + continue + song_catalog_ids.append(catalog_id) + # Obtain catalog info per 200 songs, the documented limit of 300 results in a 504 timeout + max_limit = 200 + for i in range(0, len(song_catalog_ids), max_limit): + catalog_ids = song_catalog_ids[i : i + max_limit] + catalog_endpoint = f"catalog/{self._storefront}/songs" + response = await self._get_data( + catalog_endpoint, ids=",".join(catalog_ids), include="artists,albums" + ) + for item in response["data"]: + yield self._parse_track(item) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve playlists from the provider.""" + endpoint = "me/library/playlists" + for item in await self._get_all_items(endpoint): + # Prefer catalog information over library information in case of public playlists + if item["attributes"]["hasCatalog"]: + yield await self.get_playlist(item["attributes"]["playParams"]["globalId"]) + elif item and item["id"]: + yield self._parse_playlist(item) + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}" + response = await self._get_data(endpoint, extend="editorialNotes") + return self._parse_artist(response["data"][0]) + + async def get_album(self, prov_album_id) -> Album: + """Get full album details by id.""" + endpoint = f"catalog/{self._storefront}/albums/{prov_album_id}" + response = await self._get_data(endpoint, include="artists") + return self._parse_album(response["data"][0]) + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + endpoint = f"catalog/{self._storefront}/songs/{prov_track_id}" + response = await self._get_data(endpoint, include="artists,albums") + return self._parse_track(response["data"][0]) + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + if self._is_catalog_id(prov_playlist_id): + endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}" + else: + endpoint = f"me/library/playlists/{prov_playlist_id}" + endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}" + response = await self._get_data(endpoint) + return self._parse_playlist(response["data"][0]) + + async def get_album_tracks(self, prov_album_id) -> list[Track]: + """Get all album tracks for given album id.""" + endpoint = f"catalog/{self._storefront}/albums/{prov_album_id}/tracks" + response = await self._get_data(endpoint, include="artists") + # Including albums results in a 504 error, so we need to fetch the album separately + album = await self.get_album(prov_album_id) + tracks = [] + for track_obj in response["data"]: + if "id" not in track_obj: + continue + track = self._parse_track(track_obj) + track.album = album + tracks.append(track) + return tracks + + async def get_playlist_tracks(self, prov_playlist_id, page: int = 0) -> list[Track]: + """Get all playlist tracks for given playlist id.""" + if self._is_catalog_id(prov_playlist_id): + endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}/tracks" + else: + endpoint = f"me/library/playlists/{prov_playlist_id}/tracks" + result = [] + page_size = 100 + offset = page * page_size + response = await self._get_data( + endpoint, include="artists,catalog", limit=page_size, offset=offset + ) + if not response or "data" not in response: + return result + for index, track in enumerate(response["data"]): + if track and track["id"]: + parsed_track = self._parse_track(track) + parsed_track.position = offset + index + 1 + result.append(parsed_track) + return result + + async def get_artist_albums(self, prov_artist_id) -> list[Album]: + """Get a list of all albums for the given artist.""" + endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}/albums" + try: + response = await self._get_all_items(endpoint) + except MediaNotFoundError: + # Some artists do not have albums, return empty list + self.logger.info("No albums found for artist %s", prov_artist_id) + return [] + return [self._parse_album(album) for album in response if album["id"]] + + async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: + """Get a list of 10 most popular tracks for the given artist.""" + endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}/view/top-songs" + try: + response = await self._get_data(endpoint) + except MediaNotFoundError: + # Some artists do not have top tracks, return empty list + self.logger.info("No top tracks found for artist %s", prov_artist_id) + return [] + return [self._parse_track(track) for track in response["data"] if track["id"]] + + async def library_add(self, item: MediaItemType): + """Add item to library.""" + raise NotImplementedError("Not implemented!") + + async def library_remove(self, prov_item_id, media_type: MediaType): + """Remove item from library.""" + raise NotImplementedError("Not implemented!") + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]): + """Add track(s) to playlist.""" + raise NotImplementedError("Not implemented!") + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + raise NotImplementedError("Not implemented!") + + async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + # Note, Apple music does not have an official endpoint for similar tracks. + # We will use the next-tracks endpoint to get a list of tracks that are similar to the + # provided track. However, Apple music only provides 2 tracks at a time, so we will + # need to call the endpoint multiple times. Therefore, set a limit to 6 to prevent + # flooding the apple music api. + limit = 6 + endpoint = f"me/stations/next-tracks/ra.{prov_track_id}" + found_tracks = [] + while len(found_tracks) < limit: + response = await self._post_data(endpoint, include="artists") + if not response or "data" not in response: + break + for track in response["data"]: + if track and track["id"]: + found_tracks.append(self._parse_track(track)) + return found_tracks + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + stream_metadata = await self._fetch_song_stream_metadata(item_id) + license_url = stream_metadata["hls-key-server-url"] + stream_url, uri = await self._parse_stream_url_and_uri(stream_metadata["assets"]) + if not stream_url or not uri: + raise MediaNotFoundError("No stream URL found for song.") + key_id = base64.b64decode(uri.split(",")[1]) + return StreamDetails( + item_id=item_id, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.UNKNOWN, + ), + stream_type=StreamType.ENCRYPTED_HTTP, + path=stream_url, + decryption_key=await self._get_decryption_key(license_url, key_id, uri, item_id), + can_seek=True, + ) + + def _parse_artist(self, artist_obj): + """Parse artist object to generic layout.""" + relationships = artist_obj.get("relationships", {}) + if ( + artist_obj.get("type") == "library-artists" + and relationships.get("catalog", {}).get("data", []) != [] + ): + artist_id = relationships["catalog"]["data"][0]["id"] + attributes = relationships["catalog"]["data"][0]["attributes"] + elif "attributes" in artist_obj: + artist_id = artist_obj["id"] + attributes = artist_obj["attributes"] + else: + artist_id = artist_obj["id"] + self.logger.debug("No attributes found for artist %s", artist_obj) + # No more details available other than the id, return an ItemMapping + return ItemMapping( + media_type=MediaType.ARTIST, + provider=self.instance_id, + item_id=artist_id, + name=artist_id, + ) + artist = Artist( + item_id=artist_id, + name=attributes.get("name"), + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=artist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=attributes.get("url"), + ) + }, + ) + if artwork := attributes.get("artwork"): + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if genres := attributes.get("genreNames"): + artist.metadata.genres = set(genres) + if notes := attributes.get("editorialNotes"): + artist.metadata.description = notes.get("standard") or notes.get("short") + return artist + + def _parse_album(self, album_obj: dict) -> Album | ItemMapping | None: + """Parse album object to generic layout.""" + relationships = album_obj.get("relationships", {}) + response_type = album_obj.get("type") + if ( + response_type == "library-albums" + and relationships["catalog"]["data"] != [] + and "attributes" in relationships["catalog"]["data"][0] + ): + album_id = relationships.get("catalog", {})["data"][0]["id"] + attributes = relationships.get("catalog", {})["data"][0]["attributes"] + elif "attributes" in album_obj: + album_id = album_obj["id"] + attributes = album_obj["attributes"] + else: + album_id = album_obj["id"] + # No more details available other than the id, return an ItemMapping + return ItemMapping( + media_type=MediaType.ALBUM, + provider=self.instance_id, + item_id=album_id, + name=album_id, + ) + is_available_in_catalog = attributes.get("url") is not None + if not is_available_in_catalog: + self.logger.debug( + "Skipping album %s. Album is not available in the Apple Music catalog.", + attributes.get("name"), + ) + return None + album = Album( + item_id=album_id, + provider=self.domain, + name=attributes.get("name"), + provider_mappings={ + ProviderMapping( + item_id=album_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=attributes.get("url"), + available=attributes.get("playParams", {}).get("id") is not None, + ) + }, + ) + if artists := relationships.get("artists"): + album.artists = [self._parse_artist(artist) for artist in artists["data"]] + elif artist_name := attributes.get("artistName"): + album.artists = [ + ItemMapping( + media_type=MediaType.ARTIST, + provider=self.instance_id, + item_id=artist_name, + name=artist_name, + ) + ] + if release_date := attributes.get("releaseDate"): + album.year = int(release_date.split("-")[0]) + if genres := attributes.get("genreNames"): + album.metadata.genres = set(genres) + if artwork := attributes.get("artwork"): + album.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if album_copyright := attributes.get("copyright"): + album.metadata.copyright = album_copyright + if record_label := attributes.get("recordLabel"): + album.metadata.label = record_label + if upc := attributes.get("upc"): + album.external_ids.add((ExternalID.BARCODE, "0" + upc)) + if notes := attributes.get("editorialNotes"): + album.metadata.description = notes.get("standard") or notes.get("short") + if content_rating := attributes.get("contentRating"): + album.metadata.explicit = content_rating == "explicit" + album_type = AlbumType.ALBUM + if attributes.get("isSingle"): + album_type = AlbumType.SINGLE + elif attributes.get("isCompilation"): + album_type = AlbumType.COMPILATION + album.album_type = album_type + return album + + def _parse_track( + self, + track_obj: dict[str, Any], + ) -> Track: + """Parse track object to generic layout.""" + relationships = track_obj.get("relationships", {}) + if track_obj.get("type") == "library-songs" and relationships["catalog"]["data"] != []: + track_id = relationships.get("catalog", {})["data"][0]["id"] + attributes = relationships.get("catalog", {})["data"][0]["attributes"] + elif "attributes" in track_obj: + track_id = track_obj["id"] + attributes = track_obj["attributes"] + else: + track_id = track_obj["id"] + attributes = {} + track = Track( + item_id=track_id, + provider=self.domain, + name=attributes.get("name"), + duration=attributes.get("durationInMillis", 0) / 1000, + provider_mappings={ + ProviderMapping( + item_id=track_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat(content_type=ContentType.AAC), + url=attributes.get("url"), + available=attributes.get("playParams", {}).get("id") is not None, + ) + }, + ) + if disc_number := attributes.get("discNumber"): + track.disc_number = disc_number + if track_number := attributes.get("trackNumber"): + track.track_number = track_number + # Prefer catalog information over library information for artists. + # For compilations it picks the wrong artists + if "artists" in relationships: + artists = relationships["artists"] + track.artists = [self._parse_artist(artist) for artist in artists["data"]] + # 'Similar tracks' do not provide full artist details + elif artist_name := attributes.get("artistName"): + track.artists = [ + ItemMapping( + media_type=MediaType.ARTIST, + item_id=artist_name, + provider=self.instance_id, + name=artist_name, + ) + ] + if albums := relationships.get("albums"): + if "data" in albums and len(albums["data"]) > 0: + track.album = self._parse_album(albums["data"][0]) + if artwork := attributes.get("artwork"): + track.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if genres := attributes.get("genreNames"): + track.metadata.genres = set(genres) + if composers := attributes.get("composerName"): + track.metadata.performers = set(composers.split(", ")) + if isrc := attributes.get("isrc"): + track.external_ids.add((ExternalID.ISRC, isrc)) + return track + + def _parse_playlist(self, playlist_obj) -> Playlist: + """Parse Apple Music playlist object to generic layout.""" + attributes = playlist_obj["attributes"] + playlist_id = attributes["playParams"].get("globalId") or playlist_obj["id"] + playlist = Playlist( + item_id=playlist_id, + provider=self.domain, + name=attributes["name"], + owner=attributes.get("curatorName", "me"), + provider_mappings={ + ProviderMapping( + item_id=playlist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=attributes.get("url"), + ) + }, + ) + if artwork := attributes.get("artwork"): + url = artwork["url"] + if artwork["width"] and artwork["height"]: + url = url.format(w=artwork["width"], h=artwork["height"]) + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if description := attributes.get("description"): + playlist.metadata.description = description.get("standard") + playlist.is_editable = attributes.get("canEdit", False) + if checksum := attributes.get("lastModifiedDate"): + playlist.cache_checksum = checksum + return playlist + + async def _get_all_items(self, endpoint, key="data", **kwargs) -> list[dict]: + """Get all items from a paged list.""" + limit = 50 + offset = 0 + all_items = [] + while True: + kwargs["limit"] = limit + kwargs["offset"] = offset + result = await self._get_data(endpoint, **kwargs) + if key not in result: + break + all_items += result[key] + if not result.get("next"): + break + offset += limit + return all_items + + @throttle_with_retries + async def _get_data(self, endpoint, **kwargs) -> dict[str, Any]: + """Get data from api.""" + url = f"https://api.music.apple.com/v1/{endpoint}" + headers = {"Authorization": f"Bearer {DEVELOPER_TOKEN}"} + headers["Music-User-Token"] = self._music_user_token + async with ( + self.mass.http_session.get( + url, headers=headers, params=kwargs, ssl=True, timeout=120 + ) as response, + ): + if response.status == 404 and "limit" in kwargs and "offset" in kwargs: + return {} + # Convert HTTP errors to exceptions + if response.status == 404: + raise MediaNotFoundError(f"{endpoint} not found") + if response.status == 504: + # See if we can get more info from the response on occasional timeouts + self.logger.debug( + "Apple Music API Timeout: url=%s, params=%s, response_headers=%s", + url, + kwargs, + response.headers, + ) + raise ResourceTemporarilyUnavailable("Apple Music API Timeout") + if response.status == 429: + # Debug this for now to see if the response headers give us info about the + # backoff time. There is no documentation on this. + self.logger.debug("Apple Music Rate Limiter. Headers: %s", response.headers) + raise ResourceTemporarilyUnavailable("Apple Music Rate Limiter") + response.raise_for_status() + return await response.json(loads=json_loads) + + async def _delete_data(self, endpoint, data=None, **kwargs) -> str: + """Delete data from api.""" + raise NotImplementedError("Not implemented!") + + async def _put_data(self, endpoint, data=None, **kwargs) -> str: + """Put data on api.""" + raise NotImplementedError("Not implemented!") + + @throttle_with_retries + async def _post_data(self, endpoint, data=None, **kwargs) -> str: + """Post data on api.""" + url = f"https://api.music.apple.com/v1/{endpoint}" + headers = {"Authorization": f"Bearer {DEVELOPER_TOKEN}"} + headers["Music-User-Token"] = self._music_user_token + async with ( + self.mass.http_session.post( + url, headers=headers, params=kwargs, json=data, ssl=True, timeout=120 + ) as response, + ): + # Convert HTTP errors to exceptions + if response.status == 404: + raise MediaNotFoundError(f"{endpoint} not found") + if response.status == 429: + # Debug this for now to see if the response headers give us info about the + # backoff time. There is no documentation on this. + self.logger.debug("Apple Music Rate Limiter. Headers: %s", response.headers) + raise ResourceTemporarilyUnavailable("Apple Music Rate Limiter") + response.raise_for_status() + return await response.json(loads=json_loads) + + async def _get_user_storefront(self) -> str: + """Get the user's storefront.""" + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + result = await self._get_data("me/storefront", l=language) + return result["data"][0]["id"] + + def _is_catalog_id(self, catalog_id: str) -> bool: + """Check if input is a catalog id, or a library id.""" + return catalog_id.isnumeric() or catalog_id.startswith("pl.") + + async def _fetch_song_stream_metadata(self, song_id: str) -> str: + """Get the stream URL for a song from Apple Music.""" + playback_url = "https://play.music.apple.com/WebObjects/MZPlay.woa/wa/webPlayback" + data = { + "salableAdamId": song_id, + } + async with self.mass.http_session.post( + playback_url, headers=self._get_decryption_headers(), json=data, ssl=True + ) as response: + response.raise_for_status() + content = await response.json(loads=json_loads) + return content["songList"][0] + + async def _parse_stream_url_and_uri(self, stream_assets: list[dict]) -> str: + """Parse the Stream URL and Key URI from the song.""" + ctrp256_urls = [asset["URL"] for asset in stream_assets if asset["flavor"] == "28:ctrp256"] + if len(ctrp256_urls) == 0: + raise MediaNotFoundError("No ctrp256 URL found for song.") + playlist_url = ctrp256_urls[0] + playlist_items = await fetch_playlist(self.mass, ctrp256_urls[0], raise_on_hls=False) + # Apple returns a HLS (substream) playlist but instead of chunks, + # each item is just the whole file. So we simply grab the first playlist item. + playlist_item = playlist_items[0] + # path is relative, stitch it together + base_path = playlist_url.rsplit("/", 1)[0] + track_url = base_path + "/" + playlist_items[0].path + key = playlist_item.key + return (track_url, key) + + def _get_decryption_headers(self): + """Get headers for decryption requests.""" + return { + "authorization": f"Bearer {DEVELOPER_TOKEN}", + "media-user-token": self._music_user_token, + "connection": "keep-alive", + "accept": "application/json", + "origin": "https://music.apple.com", + "referer": "https://music.apple.com/", + "accept-encoding": "gzip, deflate, br", + "content-type": "application/json;charset=utf-8", + "user-agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" + " Chrome/110.0.0.0 Safari/537.36" + ), + } + + async def _get_decryption_key( + self, license_url: str, key_id: str, uri: str, item_id: str + ) -> str: + """Get the decryption key for a song.""" + cache_key = f"decryption_key.{item_id}" + if decryption_key := await self.mass.cache.get(cache_key, base_key=self.instance_id): + self.logger.debug("Decryption key for %s found in cache.", item_id) + return decryption_key + pssh = self._get_pssh(key_id) + device = Device( + client_id=self._decrypt_client_id, + private_key=self._decrypt_private_key, + type_=DeviceTypes.ANDROID, + security_level=3, + flags={}, + ) + cdm = Cdm.from_device(device) + session_id = cdm.open() + challenge = cdm.get_license_challenge(session_id, pssh) + track_license = await self._get_license(challenge, license_url, uri, item_id) + cdm.parse_license(session_id, track_license) + key = next(key for key in cdm.get_keys(session_id) if key.type == "CONTENT") + if not key: + raise MediaNotFoundError("Unable to get decryption key for song %s.", item_id) + cdm.close(session_id) + decryption_key = key.key.hex() + self.mass.create_task( + self.mass.cache.set( + cache_key, decryption_key, expiration=7200, base_key=self.instance_id + ) + ) + return decryption_key + + def _get_pssh(self, key_id: bytes) -> PSSH: + """Get the PSSH for a song.""" + pssh_data = WidevinePsshData() + pssh_data.algorithm = 1 + pssh_data.key_ids.append(key_id) + init_data = base64.b64encode(pssh_data.SerializeToString()).decode("utf-8") + return PSSH.new(system_id=PSSH.SystemId.Widevine, init_data=init_data) + + async def _get_license(self, challenge: bytes, license_url: str, uri: str, item_id: str) -> str: + """Get the license for a song based on the challenge.""" + challenge_b64 = base64.b64encode(challenge).decode("utf-8") + data = { + "challenge": challenge_b64, + "key-system": "com.widevine.alpha", + "uri": uri, + "adamId": item_id, + "isLibrary": False, + "user-initiated": True, + } + async with self.mass.http_session.post( + license_url, data=json.dumps(data), headers=self._get_decryption_headers(), ssl=False + ) as response: + response.raise_for_status() + content = await response.json(loads=json_loads) + track_license = content.get("license") + if not track_license: + raise MediaNotFoundError("No license found for song %s.", item_id) + return track_license diff --git a/music_assistant/providers/apple_music/bin/README.md b/music_assistant/providers/apple_music/bin/README.md new file mode 100644 index 00000000..a710ed61 --- /dev/null +++ b/music_assistant/providers/apple_music/bin/README.md @@ -0,0 +1,7 @@ +# Content Decryption Module (CDM) +You need a custom CDM if you would like to playback Apple music on your local machine. The music provider expects two files to be present in your local user folder `/usr/local/bin/widevine_cdm` : + +1. client_id.bin +2. private_key.pem + +These two files allow Music Assistant to decrypt Widevine protected songs. More info on how you can obtain your own CDM files can be found [here](https://www.ismailzai.com/blog/picking-the-widevine-locks). diff --git a/music_assistant/providers/apple_music/icon.svg b/music_assistant/providers/apple_music/icon.svg new file mode 100644 index 00000000..ef11384b --- /dev/null +++ b/music_assistant/providers/apple_music/icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" aria-label="Apple Music" role="img" viewBox="0 0 512 512" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><rect width="512" height="512" rx="15%" fill="url(#g)"></rect><linearGradient id="g" x1=".5" y1=".99" x2=".5" y2=".02"><stop offset="0" stop-color="#FA233B"></stop><stop offset="1" stop-color="#FB5C74"></stop></linearGradient><path fill="#ffffff" d="M199 359V199q0-9 10-11l138-28q11-2 12 10v122q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69V88s0-20-17-15l-170 35s-13 2-13 18v203q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69"></path></g></svg> diff --git a/music_assistant/providers/apple_music/manifest.json b/music_assistant/providers/apple_music/manifest.json new file mode 100644 index 00000000..44d212f5 --- /dev/null +++ b/music_assistant/providers/apple_music/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "apple_music", + "name": "Apple Music", + "description": "Support for the Apple Music streaming provider in Music Assistant.", + "codeowners": ["@MarvinSchenkel"], + "requirements": ["pywidevine==1.8.0"], + "documentation": "https://music-assistant.io/music-providers/apple-music/", + "multi_instance": true +} diff --git a/music_assistant/providers/bluesound/__init__.py b/music_assistant/providers/bluesound/__init__.py new file mode 100644 index 00000000..bc718b4a --- /dev/null +++ b/music_assistant/providers/bluesound/__init__.py @@ -0,0 +1,405 @@ +"""Bluesound Player Provider for BluOS players to work with Music Assistant.""" + +from __future__ import annotations + +import asyncio +import time +from typing import TYPE_CHECKING, TypedDict + +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_ENABLE_ICY_METADATA, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, + ConfigEntry, + ConfigValueType, +) +from music_assistant_models.enums import PlayerFeature, PlayerState, PlayerType, ProviderFeature +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from pyblu import Player as BluosPlayer +from pyblu import Status, SyncStatus +from zeroconf import ServiceStateChange + +from music_assistant.constants import VERBOSE_LOG_LEVEL +from music_assistant.helpers.util import ( + get_port_from_zeroconf, + get_primary_ip_address_from_zeroconf, +) +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + from zeroconf.asyncio import AsyncServiceInfo + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +PLAYER_FEATURES_BASE = { + PlayerFeature.SYNC, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.PAUSE, +} + +PLAYBACK_STATE_MAP = { + "play": PlayerState.PLAYING, + "stream": PlayerState.PLAYING, + "stop": PlayerState.IDLE, + "pause": PlayerState.PAUSED, + "connecting": PlayerState.IDLE, +} + +PLAYBACK_STATE_POLL_MAP = { + "play": PlayerState.PLAYING, + "stream": PlayerState.PLAYING, + "stop": PlayerState.IDLE, + "pause": PlayerState.PAUSED, + "connecting": "CONNECTING", +} + +SOURCE_LINE_IN = "line_in" +SOURCE_AIRPLAY = "airplay" +SOURCE_SPOTIFY = "spotify" +SOURCE_UNKNOWN = "unknown" +SOURCE_RADIO = "radio" +POLL_STATE_STATIC = "static" +POLL_STATE_DYNAMIC = "dynamic" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize BluOS instance with given configuration.""" + return BluesoundPlayerProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """Set up legacy BluOS devices.""" + # ruff: noqa: ARG001 + return () + + +class BluesoundDiscoveryInfo(TypedDict): + """Template for MDNS discovery info.""" + + _objectType: str + ip_address: str + port: str + mac: str + model: str + zs: bool + + +class BluesoundPlayer: + """Holds the details of the (discovered) BluOS player.""" + + def __init__( + self, + prov: BluesoundPlayerProvider, + player_id: str, + discovery_info: BluesoundDiscoveryInfo, + ip_address: str, + port: int, + ) -> None: + """Initialize the BluOS Player.""" + self.port = port + self.prov = prov + self.mass = prov.mass + self.player_id = player_id + self.discovery_info = discovery_info + self.ip_address = ip_address + self.logger = prov.logger.getChild(player_id) + self.connected: bool = True + self.client = BluosPlayer(self.ip_address, self.port, self.mass.http_session) + self.sync_status = SyncStatus + self.status = Status + self.poll_state = POLL_STATE_STATIC + self.dynamic_poll_count: int = 0 + self.mass_player: Player | None = None + self._listen_task: asyncio.Task | None = None + + async def disconnect(self) -> None: + """Disconnect the BluOS client and cleanup.""" + if self._listen_task and not self._listen_task.done(): + self._listen_task.cancel() + if self.client: + await self.client.close() + self.connected = False + self.logger.debug("Disconnected from player API") + + async def update_attributes(self) -> None: + """Update the BluOS player attributes.""" + self.logger.debug("updating %s attributes", self.player_id) + if self.dynamic_poll_count > 0: + self.dynamic_poll_count -= 1 + + self.sync_status = await self.client.sync_status() + self.status = await self.client.status() + + # Update timing + self.mass_player.elapsed_time = self.status.seconds + self.mass_player.elapsed_time_last_updated = time.time() + + if not self.mass_player: + return + if self.sync_status.volume == -1: + self.mass_player.volume_level = 100 + else: + self.mass_player.volume_level = self.sync_status.volume + self.mass_player.volume_muted = self.status.mute + + self.logger.log( + VERBOSE_LOG_LEVEL, + "Speaker state: %s vs reported state: %s", + PLAYBACK_STATE_POLL_MAP[self.status.state], + self.mass_player.state, + ) + + if ( + self.poll_state == POLL_STATE_DYNAMIC and self.dynamic_poll_count <= 0 + ) or self.mass_player.state == PLAYBACK_STATE_POLL_MAP[self.status.state]: + self.logger.debug("Changing bluos poll state from %s to static", self.poll_state) + self.poll_state = POLL_STATE_STATIC + self.mass_player.poll_interval = 30 + self.mass.players.update(self.player_id) + + if self.status.state == "stream": + mass_active = self.mass.streams.base_url + elif self.status.state == "stream" and self.status.input_id == "input0": + self.mass_player.active_source = SOURCE_LINE_IN + elif self.status.state == "stream" and self.status.input_id == "Airplay": + self.mass_player.active_source = SOURCE_AIRPLAY + elif self.status.state == "stream" and self.status.input_id == "Spotify": + self.mass_player.active_source = SOURCE_SPOTIFY + elif self.status.state == "stream" and self.status.input_id == "RadioParadise": + self.mass_player.active_source = SOURCE_RADIO + elif self.status.state == "stream" and (mass_active not in self.status.stream_url): + self.mass_player.active_source = SOURCE_UNKNOWN + + # TODO check pair status + + # TODO fix pairing + + if self.sync_status.master is None: + if self.sync_status.slaves: + self.mass_player.group_childs = ( + self.sync_status.slaves if len(self.sync_status.slaves) > 1 else set() + ) + self.mass_player.synced_to = None + + if self.status.state == "stream": + self.mass_player.current_media = PlayerMedia( + uri=self.status.stream_url, + title=self.status.name, + artist=self.status.artist, + album=self.status.album, + image_url=self.status.image, + ) + else: + self.mass_player.current_media = None + + else: + self.mass_player.group_childs = set() + self.mass_player.synced_to = self.sync_status.master + self.mass_player.active_source = self.sync_status.master + + self.mass_player.state = PLAYBACK_STATE_MAP[self.status.state] + self.mass.players.update(self.player_id) + + +class BluesoundPlayerProvider(PlayerProvider): + """Bluos compatible player provider, providing support for bluesound speakers.""" + + bluos_players: dict[str, BluesoundPlayer] + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.bluos_players: dict[str, BluesoundPlayer] = {} + + async def on_mdns_service_state_change( + self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None + ) -> None: + """Handle MDNS service state callback for BluOS.""" + name = name.split(".", 1)[0] + self.player_id = info.decoded_properties["mac"] + # Handle removed player + + if state_change == ServiceStateChange.Removed: + # Check if the player manager has an existing entry for this player + if mass_player := self.mass.players.get(self.player_id): + # The player has become unavailable + self.logger.debug("Player offline: %s", mass_player.display_name) + mass_player.available = False + self.mass.players.update(self.player_id) + return + + if bluos_player := self.bluos_players.get(self.player_id): + if mass_player := self.mass.players.get(self.player_id): + cur_address = get_primary_ip_address_from_zeroconf(info) + cur_port = get_port_from_zeroconf(info) + if cur_address and cur_address != mass_player.device_info.address: + self.logger.debug( + "Address updated to %s for player %s", cur_address, mass_player.display_name + ) + bluos_player.ip_address = cur_address + bluos_player.port = cur_port + mass_player.device_info = DeviceInfo( + model=mass_player.device_info.model, + manufacturer=mass_player.device_info.manufacturer, + address=str(cur_address), + ) + if not mass_player.available: + self.logger.debug("Player back online: %s", mass_player.display_name) + bluos_player.client.sync() + bluos_player.discovery_info = info + self.mass.players.update(self.player_id) + return + # handle new player + cur_address = get_primary_ip_address_from_zeroconf(info) + cur_port = get_port_from_zeroconf(info) + self.logger.debug("Discovered device %s on %s", name, cur_address) + + self.bluos_players[self.player_id] = bluos_player = BluesoundPlayer( + self, self.player_id, discovery_info=info, ip_address=cur_address, port=cur_port + ) + + bluos_player.mass_player = mass_player = Player( + player_id=self.player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=name, + available=True, + powered=True, + device_info=DeviceInfo( + model="BluOS speaker", + manufacturer="Bluesound", + address=cur_address, + ), + # Set the supported features for this player + supported_features=( + PlayerFeature.VOLUME_SET, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.PAUSE, + ), + needs_poll=True, + poll_interval=30, + ) + await self.mass.players.register(mass_player) + + # TODO sync + await bluos_player.update_attributes() + self.mass.players.update(self.player_id) + + async def get_player_config_entries( + self, + player_id: str, + ) -> tuple[ConfigEntry, ...]: + """Return Config Entries for the given player.""" + base_entries = await super().get_player_config_entries(self.player_id) + if not self.bluos_players.get(player_id): + # TODO fix player entries + return (*base_entries, CONF_ENTRY_CROSSFADE) + return ( + *base_entries, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_ENABLE_ICY_METADATA, + ) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + play_state = await bluos_player.client.stop(timeout=1) + if play_state == "stop": + bluos_player.poll_state = POLL_STATE_DYNAMIC + bluos_player.dynamic_poll_count = 6 + bluos_player.mass_player.poll_interval = 0.5 + # Update media info then optimistically override playback state and source + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + play_state = await bluos_player.client.play(timeout=1) + if play_state == "stream": + bluos_player.poll_state = POLL_STATE_DYNAMIC + bluos_player.dynamic_poll_count = 6 + bluos_player.mass_player.poll_interval = 0.5 + # Optimistic state, reduces interface lag + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + play_state = await bluos_player.client.pause(timeout=1) + if play_state == "pause": + bluos_player.poll_state = POLL_STATE_DYNAMIC + bluos_player.dynamic_poll_count = 6 + bluos_player.mass_player.poll_interval = 0.5 + self.logger.debug("Set BluOS state to %s", play_state) + # Optimistic state, reduces interface lag + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + await bluos_player.client.volume(level=volume_level, timeout=1) + self.logger.debug("Set BluOS speaker volume to %s", volume_level) + mass_player = self.mass.players.get(player_id) + # Optimistic state, reduces interface lag + mass_player.volume_level = volume_level + await bluos_player.update_attributes() + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + await bluos_player.client.volume(mute=muted) + # Optimistic state, reduces interface lag + mass_player = self.mass.players.get(player_id) + mass_player.volume_mute = muted + await bluos_player.update_attributes() + + async def play_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle PLAY MEDIA for BluOS player using the provided URL.""" + self.logger.debug("Play_media called") + if bluos_player := self.bluos_players[player_id]: + self.mass.players.update(player_id) + play_state = await bluos_player.client.play_url(media.uri, timeout=1) + # Enable dynamic polling + if play_state == "stream": + bluos_player.poll_state = POLL_STATE_DYNAMIC + bluos_player.dynamic_poll_count = 6 + bluos_player.mass_player.poll_interval = 0.5 + self.logger.debug("Set BluOS state to %s", play_state) + await bluos_player.update_attributes() + + # Optionally, handle the playback_state or additional logic here + if play_state in ("PlayerUnexpectedResponseError", "PlayerUnreachableError"): + raise PlayerCommandFailed("Failed to start playback.") + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + if bluos_player := self.bluos_players[player_id]: + await bluos_player.update_attributes() + + # TODO fix sync & unsync + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for BluOS player.""" + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for BluOS player.""" + if bluos_player := self.bluos_players[player_id]: + await bluos_player.client.player.leave_group() diff --git a/music_assistant/providers/bluesound/icon.svg b/music_assistant/providers/bluesound/icon.svg new file mode 100644 index 00000000..2cb9d37b --- /dev/null +++ b/music_assistant/providers/bluesound/icon.svg @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 300 300" + version="1.1" + id="svg2" + sodipodi:docname="bluos.svg" + xml:space="preserve" + inkscape:export-filename="bluos.svg" + inkscape:export-xdpi="48" + inkscape:export-ydpi="48" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><defs + id="defs2" /><sodipodi:namedview + id="namedview2" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showguides="true" + inkscape:zoom="3.9095958" + inkscape:cx="289.92767" + inkscape:cy="123.54218" + inkscape:window-width="2880" + inkscape:window-height="1715" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><rect + style="fill:#000000" + id="rect3" + width="300" + height="300" + x="0" + y="0" + inkscape:label="rect3" /><path + fill="currentColor" + d="m 169.36756,174.07934 -69.882704,0.025 a 37.862849,37.862849 0 0 0 -37.83788,37.81291 c 0,17.06283 0.0583,31.1043 0.0583,31.1043 v 6.85843 H 169.53407 l 1.58976,-0.0166 c 38.93656,-0.82401 67.22757,-29.77257 67.22757,-68.85895 a 72.68768,72.68768 0 0 0 -6.70028,-30.97112 72.995643,72.995643 0 0 0 6.70028,-30.9961 c 0,-39.078052 -28.29101,-68.026593 -67.28584,-68.875573 L 61.705276,50.120017 v 6.933321 c 0,0 -0.0583,13.991525 -0.0583,31.071007 a 37.854525,37.854525 0 0 0 37.83788,37.796255 l 69.907674,0.10821 c 19.76792,0 36.55609,8.72285 46.33601,24.0045 -9.80489,15.29829 -26.6097,24.04611 -46.36098,24.04611 m 1.82281,-62.02549 -71.705514,0.0166 A 23.971203,23.971203 0 0 1 75.521976,88.124269 V 63.978276 h 93.870554 c 31.91999,0 55.10048,23.155516 55.10048,55.058854 0,5.66819 -0.76575,11.14494 -2.25563,16.39697 -12.2353,-14.36608 -30.53831,-22.88917 -51.04701,-23.38025 m 51.04701,52.55354 c 1.48988,5.23537 2.25563,10.7371 2.25563,16.39696 0,31.8867 -23.18049,55.02557 -55.10048,55.02557 l -93.870554,0.0166 v -24.12935 a 23.98785,23.98785 0 0 1 23.96288,-23.96288 h 70.298874 l 2.39712,-0.12485 c 20.07588,-0.69916 37.9877,-9.05579 50.05653,-23.2221" + id="path1" + style="fill:#ffffff;stroke-width:8.32333" /></svg> diff --git a/music_assistant/providers/bluesound/manifest.json b/music_assistant/providers/bluesound/manifest.json new file mode 100644 index 00000000..3379858e --- /dev/null +++ b/music_assistant/providers/bluesound/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "player", + "domain": "bluesound", + "name": "Bluesound", + "description": "BluOS Player provider for Music Assistant.", + "codeowners": ["@cyanogenbot"], + "requirements": ["pyblu==1.0.4"], + "documentation": "https://music-assistant.io/player-support/bluesound/", + "mdns_discovery": ["_musc._tcp.local."] +} diff --git a/music_assistant/providers/builtin/__init__.py b/music_assistant/providers/builtin/__init__.py new file mode 100644 index 00000000..b9b30201 --- /dev/null +++ b/music_assistant/providers/builtin/__init__.py @@ -0,0 +1,644 @@ +"""Built-in/generic provider to handle media from files and (remote) urls.""" + +from __future__ import annotations + +import asyncio +import os +import time +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING, NotRequired, TypedDict, cast + +import aiofiles +import shortuuid +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ( + CacheCategory, + ConfigEntryType, + ContentType, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import ( + InvalidDataError, + MediaNotFoundError, + ProviderUnavailableError, +) +from music_assistant_models.helpers.uri import parse_uri +from music_assistant_models.media_items import ( + Artist, + AudioFormat, + MediaItemImage, + MediaItemMetadata, + MediaItemType, + Playlist, + ProviderMapping, + Radio, + Track, + UniqueList, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import MASS_LOGO, RESOURCES_DIR, VARIOUS_ARTISTS_FANART +from music_assistant.helpers.tags import AudioTags, parse_tags +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +class StoredItem(TypedDict): + """Definition of an media item (for the builtin provider) stored in persistent storage.""" + + item_id: str # url or (locally accessible) file path (or id in case of playlist) + name: str + image_url: NotRequired[str] + last_updated: NotRequired[int] + + +CONF_KEY_RADIOS = "stored_radios" +CONF_KEY_TRACKS = "stored_tracks" +CONF_KEY_PLAYLISTS = "stored_playlists" + + +ALL_FAVORITE_TRACKS = "all_favorite_tracks" +RANDOM_ARTIST = "random_artist" +RANDOM_ALBUM = "random_album" +RANDOM_TRACKS = "random_tracks" +RECENTLY_PLAYED = "recently_played" + +BUILTIN_PLAYLISTS = { + ALL_FAVORITE_TRACKS: "All favorited tracks", + RANDOM_ARTIST: "Random Artist (from library)", + RANDOM_ALBUM: "Random Album (from library)", + RANDOM_TRACKS: "500 Random tracks (from library)", + RECENTLY_PLAYED: "Recently played tracks", +} + +COLLAGE_IMAGE_PLAYLISTS = (ALL_FAVORITE_TRACKS, RANDOM_TRACKS) + +DEFAULT_THUMB = MediaItemImage( + type=ImageType.THUMB, + path=MASS_LOGO, + provider="builtin", + remotely_accessible=False, +) + +DEFAULT_FANART = MediaItemImage( + type=ImageType.FANART, + path=VARIOUS_ARTISTS_FANART, + provider="builtin", + remotely_accessible=False, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return BuiltinProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, # noqa: ARG001 + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + return tuple( + ConfigEntry( + key=key, + type=ConfigEntryType.BOOLEAN, + label=name, + default_value=True, + category="builtin_playlists", + ) + for key, name in BUILTIN_PLAYLISTS.items() + ) + + +class BuiltinProvider(MusicProvider): + """Built-in/generic provider to handle (manually added) media from files and (remote) urls.""" + + _playlists_dir: str + _playlist_lock: asyncio.Lock + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + self._playlist_lock = asyncio.Lock() + # make sure that our directory with collage images exists + self._playlists_dir = os.path.join(self.mass.storage_path, "playlists") + if not await asyncio.to_thread(os.path.exists, self._playlists_dir): + await asyncio.to_thread(os.mkdir, self._playlists_dir) + await super().loaded_in_mass() + # migrate old image path + # TODO: remove this after 2.3+ release + old_path = ( + "/usr/local/lib/python3.12/site-packages/music_assistant/server/helpers/resources" + ) + new_path = str(RESOURCES_DIR) + query = ( + "UPDATE playlists SET metadata = " + f"REPLACE (metadata, '{old_path}', '{new_path}') " + f"WHERE playlists.metadata LIKE '%{old_path}%'" + ) + if self.mass.music.database: + await self.mass.music.database.execute(query) + await self.mass.music.database.commit() + + @property + def is_streaming_provider(self) -> bool: + """Return True if the provider is a streaming provider.""" + return False + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return ( + ProviderFeature.BROWSE, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_RADIOS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.LIBRARY_RADIOS_EDIT, + ProviderFeature.PLAYLIST_CREATE, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + parsed_item = cast(Track, await self.parse_item(prov_track_id)) + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_TRACKS, []) + if stored_item := next((x for x in stored_items if x["item_id"] == prov_track_id), None): + # always prefer the stored info, such as the name + parsed_item.name = stored_item["name"] + if image_url := stored_item.get("image_url"): + parsed_item.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.domain, + remotely_accessible=image_url.startswith("http"), + ) + ] + ) + return parsed_item + + async def get_radio(self, prov_radio_id: str) -> Radio: + """Get full radio details by id.""" + parsed_item = await self.parse_item(prov_radio_id, force_radio=True) + assert isinstance(parsed_item, Radio) + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_RADIOS, []) + if stored_item := next((x for x in stored_items if x["item_id"] == prov_radio_id), None): + # always prefer the stored info, such as the name + parsed_item.name = stored_item["name"] + if image_url := stored_item.get("image_url"): + parsed_item.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.domain, + remotely_accessible=image_url.startswith("http"), + ) + ] + ) + return parsed_item + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + artist = prov_artist_id + # this is here for compatibility reasons only + return Artist( + item_id=artist, + provider=self.domain, + name=artist, + provider_mappings={ + ProviderMapping( + item_id=artist, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=False, + ) + }, + ) + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get full playlist details by id.""" + if prov_playlist_id in BUILTIN_PLAYLISTS: + # this is one of our builtin/default playlists + return Playlist( + item_id=prov_playlist_id, + provider=self.instance_id, + name=BUILTIN_PLAYLISTS[prov_playlist_id], + provider_mappings={ + ProviderMapping( + item_id=prov_playlist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + owner="Music Assistant", + is_editable=False, + cache_checksum=str(int(time.time())), + metadata=MediaItemMetadata( + images=UniqueList([DEFAULT_THUMB]) + if prov_playlist_id in COLLAGE_IMAGE_PLAYLISTS + else UniqueList([DEFAULT_THUMB, DEFAULT_FANART]), + ), + ) + # user created universal playlist + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) + stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) + if not stored_item: + raise MediaNotFoundError + playlist = Playlist( + item_id=prov_playlist_id, + provider=self.instance_id, + name=stored_item["name"], + provider_mappings={ + ProviderMapping( + item_id=prov_playlist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + owner="Music Assistant", + is_editable=True, + ) + playlist.cache_checksum = str(stored_item.get("last_updated")) + if image_url := stored_item.get("image_url"): + playlist.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.domain, + remotely_accessible=image_url.startswith("http"), + ) + ] + ) + return playlist + + async def get_item(self, media_type: MediaType, prov_item_id: str) -> MediaItemType: + """Get single MediaItem from provider.""" + if media_type == MediaType.ARTIST: + return await self.get_artist(prov_item_id) + if media_type == MediaType.TRACK: + return await self.get_track(prov_item_id) + if media_type == MediaType.RADIO: + return await self.get_radio(prov_item_id) + if media_type == MediaType.PLAYLIST: + return await self.get_playlist(prov_item_id) + if media_type == MediaType.UNKNOWN: + return await self.parse_item(prov_item_id) + raise NotImplementedError + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_TRACKS, []) + for item in stored_items: + yield await self.get_track(item["item_id"]) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve library/subscribed playlists from the provider.""" + # return user stored playlists + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) + for item in stored_items: + yield await self.get_playlist(item["item_id"]) + # return builtin playlists + for item_id in BUILTIN_PLAYLISTS: + if self.config.get_value(item_id) is False: + continue + yield await self.get_playlist(item_id) + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_RADIOS, []) + for item in stored_items: + yield await self.get_radio(item["item_id"]) + + async def library_add(self, item: MediaItemType) -> bool: + """Add item to provider's library. Return true on success.""" + if item.media_type == MediaType.TRACK: + key = CONF_KEY_TRACKS + elif item.media_type == MediaType.RADIO: + key = CONF_KEY_RADIOS + else: + return False + stored_item = StoredItem(item_id=item.item_id, name=item.name) + if item.image: + stored_item["image_url"] = item.image.path + stored_items: list[StoredItem] = self.mass.config.get(key, []) + # filter out existing + stored_items = [x for x in stored_items if x["item_id"] != item.item_id] + stored_items.append(stored_item) + self.mass.config.set(key, stored_items) + return True + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove item from provider's library. Return true on success.""" + if media_type == MediaType.PLAYLIST and prov_item_id in BUILTIN_PLAYLISTS: + # user wants to disable/remove one of our builtin playlists + # to prevent it comes back, we mark it as disabled in config + self.mass.config.set_raw_provider_config_value(self.instance_id, prov_item_id, False) + return True + if media_type == MediaType.TRACK: + # regular manual track URL/path + key = CONF_KEY_TRACKS + elif media_type == MediaType.RADIO: + # regular manual radio URL/path + key = CONF_KEY_RADIOS + elif media_type == MediaType.PLAYLIST: + # manually added (multi provider) playlist removal + key = CONF_KEY_PLAYLISTS + else: + return False + stored_items: list[StoredItem] = self.mass.config.get(key, []) + stored_items = [x for x in stored_items if x["item_id"] != prov_item_id] + self.mass.config.set(key, stored_items) + return True + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + if page > 0: + # paging not supported, we always return the whole list at once + return [] + if prov_playlist_id in BUILTIN_PLAYLISTS: + return await self._get_builtin_playlist_tracks(prov_playlist_id) + # user created universal playlist + result: list[Track] = [] + playlist_items = await self._read_playlist_file_items(prov_playlist_id) + for index, uri in enumerate(playlist_items, 1): + try: + media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) + media_controller = self.mass.music.get_controller(media_type) + # prefer item already in the db + track = await media_controller.get_library_item_by_prov_id( + item_id, provider_instance_id_or_domain + ) + if track is None: + # get the provider item and not the full track from a regular 'get' call + # as we only need basic track info here + track = await media_controller.get_provider_item( + item_id, provider_instance_id_or_domain + ) + assert isinstance(track, Track) + track.position = index + result.append(track) + except (MediaNotFoundError, InvalidDataError, ProviderUnavailableError) as err: + self.logger.warning( + "Skipping %s in playlist %s: %s", uri, prov_playlist_id, str(err) + ) + return result + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + playlist_items = await self._read_playlist_file_items(prov_playlist_id) + for uri in prov_track_ids: + if uri not in playlist_items: + playlist_items.append(uri) + # store playlist file + await self._write_playlist_file_items(prov_playlist_id, playlist_items) + # mark last_updated on playlist object + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) + stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) + if stored_item: + stored_item["last_updated"] = int(time.time()) + self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + playlist_items = await self._read_playlist_file_items(prov_playlist_id) + # remove items by index + for i in sorted(positions_to_remove, reverse=True): + del playlist_items[i - 1] + # store playlist file + await self._write_playlist_file_items(prov_playlist_id, playlist_items) + # mark last_updated on playlist object + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) + stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) + if stored_item: + stored_item["last_updated"] = int(time.time()) + self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) + + async def create_playlist(self, name: str) -> Playlist: + """Create a new playlist on provider with given name.""" + item_id = shortuuid.random(8) + stored_item = StoredItem(item_id=item_id, name=name) + stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) + stored_items.append(stored_item) + self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) + return await self.get_playlist(item_id) + + async def parse_item( + self, + url: str, + force_refresh: bool = False, + force_radio: bool = False, + ) -> Track | Radio: + """Parse plain URL to MediaItem of type Radio or Track.""" + try: + media_info = await self._get_media_info(url, force_refresh) + except Exception as err: + raise MediaNotFoundError from err + is_radio = media_info.get("icyname") or not media_info.duration + provider_mappings = { + ProviderMapping( + item_id=url, + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(media_info.format), + sample_rate=media_info.sample_rate, + bit_depth=media_info.bits_per_sample, + bit_rate=media_info.bit_rate, + ), + ) + } + media_item: Track | Radio + if is_radio or force_radio: + # treat as radio + media_item = Radio( + item_id=url, + provider=self.domain, + name=media_info.get("icyname") + or media_info.get("programtitle") + or media_info.title + or url, + provider_mappings=provider_mappings, + ) + else: + media_item = Track( + item_id=url, + provider=self.domain, + name=media_info.title or url, + duration=int(media_info.duration or 0), + artists=UniqueList( + [await self.get_artist(artist) for artist in media_info.artists] + ), + provider_mappings=provider_mappings, + ) + + if media_info.has_cover_image: + media_item.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=url, + provider=self.domain, + remotely_accessible=False, + ) + ] + ) + return media_item + + async def _get_media_info(self, url: str, force_refresh: bool = False) -> AudioTags: + """Retrieve mediainfo for url.""" + cache_category = CacheCategory.MEDIA_INFO + cache_base_key = self.lookup_key + # do we have some cached info for this url ? + cached_info = await self.mass.cache.get( + url, category=cache_category, base_key=cache_base_key + ) + if cached_info and not force_refresh: + return AudioTags.parse(cached_info) + # parse info with ffprobe (and store in cache) + media_info = await parse_tags(url) + if "authSig" in url: + media_info.has_cover_image = False + await self.mass.cache.set( + url, media_info.raw, category=cache_category, base_key=cache_base_key + ) + return media_info + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track/radio.""" + media_info = await self._get_media_info(item_id) + is_radio = media_info.get("icy-name") or not media_info.duration + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(media_info.format), + sample_rate=media_info.sample_rate, + bit_depth=media_info.bits_per_sample, + channels=media_info.channels, + ), + media_type=MediaType.RADIO if is_radio else MediaType.TRACK, + stream_type=StreamType.HTTP, + path=item_id, + can_seek=not is_radio, + ) + + async def _get_builtin_playlist_random_favorite_tracks(self) -> list[Track]: + result: list[Track] = [] + res = await self.mass.music.tracks.library_items( + favorite=True, limit=250000, order_by="random_play_count" + ) + for idx, item in enumerate(res, 1): + item.position = idx + result.append(item) + return result + + async def _get_builtin_playlist_random_tracks(self) -> list[Track]: + result: list[Track] = [] + res = await self.mass.music.tracks.library_items(limit=500, order_by="random_play_count") + for idx, item in enumerate(res, 1): + item.position = idx + result.append(item) + return result + + async def _get_builtin_playlist_random_album(self) -> UniqueList[Track]: + for in_library_only in (True, False): + for min_tracks_required in (10, 5, 1): + for random_album in await self.mass.music.albums.library_items( + limit=25, order_by="random" + ): + tracks = await self.mass.music.albums.tracks( + random_album.item_id, random_album.provider, in_library_only=in_library_only + ) + if len(tracks) < min_tracks_required: + continue + for idx, track in enumerate(tracks, 1): + track.position = idx + return tracks + return UniqueList() + + async def _get_builtin_playlist_random_artist(self) -> UniqueList[Track]: + for in_library_only in (True, False): + for min_tracks_required in (25, 10, 5, 1): + for random_artist in await self.mass.music.artists.library_items( + limit=25, order_by="random" + ): + tracks = await self.mass.music.artists.tracks( + random_artist.item_id, + random_artist.provider, + in_library_only=in_library_only, + ) + if len(tracks) < min_tracks_required: + continue + for idx, track in enumerate(tracks, 1): + track.position = idx + return tracks + return UniqueList() + + async def _get_builtin_playlist_recently_played(self) -> list[Track]: + result: list[Track] = [] + recent_tracks = await self.mass.music.recently_played(100, [MediaType.TRACK]) + for idx, track in enumerate(recent_tracks, 1): + assert isinstance(track, Track) + track.position = idx + result.append(track) + return result + + async def _get_builtin_playlist_tracks( + self, builtin_playlist_id: str + ) -> list[Track] | UniqueList[Track]: + """Get all playlist tracks for given builtin playlist id.""" + try: + return await { + ALL_FAVORITE_TRACKS: self._get_builtin_playlist_random_favorite_tracks, + RANDOM_TRACKS: self._get_builtin_playlist_random_tracks, + RANDOM_ALBUM: self._get_builtin_playlist_random_album, + RANDOM_ARTIST: self._get_builtin_playlist_random_artist, + RECENTLY_PLAYED: self._get_builtin_playlist_recently_played, + }[builtin_playlist_id]() + except KeyError: + raise MediaNotFoundError(f"No built in playlist: {builtin_playlist_id}") + + async def _read_playlist_file_items(self, playlist_id: str) -> list[str]: + """Return lines of a playlist file.""" + playlist_file = os.path.join(self._playlists_dir, playlist_id) + if not await asyncio.to_thread(os.path.isfile, playlist_file): + return [] + async with ( + self._playlist_lock, + aiofiles.open(playlist_file, encoding="utf-8") as _file, + ): + lines = await _file.readlines() + return [x.strip() for x in lines] + + async def _write_playlist_file_items(self, playlist_id: str, lines: list[str]) -> None: + """Return lines of a playlist file.""" + playlist_file = os.path.join(self._playlists_dir, playlist_id) + async with ( + self._playlist_lock, + aiofiles.open(playlist_file, "w", encoding="utf-8") as _file, + ): + await _file.write("\n".join(lines)) diff --git a/music_assistant/providers/builtin/icon.svg b/music_assistant/providers/builtin/icon.svg new file mode 100644 index 00000000..845920ca --- /dev/null +++ b/music_assistant/providers/builtin/icon.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 25 25" version="1.1"> +<g id="surface1"> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 1.5 0 L 23.5 0 C 24.328125 0 25 0.671875 25 1.5 L 25 23.5 C 25 24.328125 24.328125 25 23.5 25 L 1.5 25 C 0.671875 25 0 24.328125 0 23.5 L 0 1.5 C 0 0.671875 0.671875 0 1.5 0 Z M 1.5 0 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 10.386719 18.875 L 14.8125 7.125 L 16.113281 7.125 L 11.6875 18.875 Z M 10.386719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 21.371094 18.875 L 16.945312 7.125 L 18.246094 7.125 L 22.671875 18.875 Z M 21.371094 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.636719 18.875 L 2.636719 7.125 L 3.875 7.125 L 3.875 18.875 Z M 2.636719 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 5.445312 18.875 L 5.445312 7.125 L 6.683594 7.125 L 6.683594 18.875 Z M 5.445312 18.875 "/> +<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 8.253906 18.875 L 8.253906 7.125 L 9.492188 7.125 L 9.492188 18.875 Z M 8.253906 18.875 "/> +</g> +</svg> diff --git a/music_assistant/providers/builtin/manifest.json b/music_assistant/providers/builtin/manifest.json new file mode 100644 index 00000000..dc489276 --- /dev/null +++ b/music_assistant/providers/builtin/manifest.json @@ -0,0 +1,12 @@ +{ + "type": "music", + "domain": "builtin", + "name": "Music Assistant", + "description": "Built-in/generic provider that handles generic urls and playlists.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/music-providers/builtin/", + "multi_instance": false, + "builtin": true, + "allow_disable": false +} diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py new file mode 100644 index 00000000..58898ed8 --- /dev/null +++ b/music_assistant/providers/chromecast/__init__.py @@ -0,0 +1,749 @@ +"""Chromecast Player provider for Music Assistant, utilizing the pychromecast library.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import threading +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any +from uuid import UUID + +import pychromecast +from music_assistant_models.config_entries import ( + BASE_PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_ENFORCE_MP3, + ConfigEntry, + ConfigValueType, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import MediaType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from pychromecast.controllers.media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE, MediaController +from pychromecast.controllers.multizone import MultizoneController, MultizoneManager +from pychromecast.discovery import CastBrowser, SimpleCastListener +from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED + +from music_assistant.constants import ( + CONF_ENFORCE_MP3, + CONF_PLAYERS, + MASS_LOGO_ONLINE, + VERBOSE_LOG_LEVEL, +) +from music_assistant.helpers import CastStatusListener, ChromecastInfo +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + from pychromecast.controllers.media import MediaStatus + from pychromecast.controllers.receiver import CastStatus + from pychromecast.models import CastInfo + from pychromecast.socket_client import ConnectionStatus + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +PLAYER_CONFIG_ENTRIES = ( + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3, +) + +# originally/officially cast supports 96k sample rate (even for groups) +# but it seems a (recent?) update broke this ?! +# For now only set safe default values and let the user try out higher values +CONF_ENTRY_SAMPLE_RATES_CAST = create_sample_rates_config_entry(96000, 24, 48000, 24) +CONF_ENTRY_SAMPLE_RATES_CAST_GROUP = create_sample_rates_config_entry(96000, 24, 44100, 16) + + +MASS_APP_ID = "C35B0678" + + +# Monkey patch the Media controller here to store the queue items +_patched_process_media_status_org = MediaController._process_media_status + + +def _patched_process_media_status(self, data) -> None: + """Process STATUS message(s) of the media controller.""" + _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 + + +MediaController._process_media_status = _patched_process_media_status + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return ChromecastProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return () # we do not have any config entries (yet) + + +@dataclass +class CastPlayer: + """Wrapper around Chromecast with some additional attributes.""" + + player_id: str + cast_info: ChromecastInfo + cc: pychromecast.Chromecast + player: Player + status_listener: CastStatusListener | None = None + mz_controller: MultizoneController | None = None + active_group: str | None = None + last_poll: float = 0 + flow_meta_checksum: str | None = None + + +class ChromecastProvider(PlayerProvider): + """Player provider for Chromecast based players.""" + + mz_mgr: MultizoneManager | None = None + browser: CastBrowser | None = None + castplayers: dict[str, CastPlayer] + _discover_lock: threading.Lock + + def __init__( + self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig + ) -> None: + """Handle async initialization of the provider.""" + super().__init__(mass, manifest, config) + self._discover_lock = threading.Lock() + self.castplayers = {} + self.mz_mgr = MultizoneManager() + self.browser = CastBrowser( + SimpleCastListener( + add_callback=self._on_chromecast_discovered, + remove_callback=self._on_chromecast_removed, + update_callback=self._on_chromecast_discovered, + ), + self.mass.aiozc.zeroconf, + ) + # set-up pychromecast logging + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("pychromecast").setLevel(logging.DEBUG) + else: + logging.getLogger("pychromecast").setLevel(self.logger.level + 10) + + async def discover_players(self) -> None: + """Discover Cast players on the network.""" + # start discovery in executor + await self.mass.loop.run_in_executor(None, self.browser.start_discovery) + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + if not self.browser: + return + + # stop discovery + def stop_discovery() -> None: + """Stop the chromecast discovery threads.""" + if self.browser._zc_browser: + with contextlib.suppress(RuntimeError): + self.browser._zc_browser.cancel() + + self.browser.host_browser.stop.set() + self.browser.host_browser.join() + + await self.mass.loop.run_in_executor(None, stop_discovery) + # stop all chromecasts + for castplayer in list(self.castplayers.values()): + await self._disconnect_chromecast(castplayer) + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + cast_player = self.castplayers.get(player_id) + if cast_player and cast_player.player.type == PlayerType.GROUP: + return ( + *BASE_PLAYER_CONFIG_ENTRIES, + *PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_SAMPLE_RATES_CAST_GROUP, + ) + base_entries = await super().get_player_config_entries(player_id) + return (*base_entries, *PLAYER_CONFIG_ENTRIES, CONF_ENTRY_SAMPLE_RATES_CAST) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.media_controller.stop) + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.media_controller.play) + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.media_controller.pause) + + async def cmd_next(self, player_id: str) -> None: + """Handle NEXT TRACK command for given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.media_controller.queue_next) + + async def cmd_previous(self, player_id: str) -> None: + """Handle PREVIOUS TRACK command for given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.media_controller.queue_prev) + + async def cmd_power(self, player_id: str, powered: bool) -> None: + """Send POWER command to given player.""" + castplayer = self.castplayers[player_id] + if powered: + await self._launch_app(castplayer) + else: + castplayer.player.active_group = None + castplayer.player.active_source = None + await asyncio.to_thread(castplayer.cc.quit_app) + # optimistically update the group childs + if castplayer.player.type == PlayerType.GROUP: + active_group = castplayer.player.active_group or castplayer.player.player_id + for child_id in castplayer.player.group_childs: + if child := self.castplayers.get(child_id): + child.player.powered = powered + child.player.active_group = active_group if powered else None + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.set_volume, volume_level / 100) + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + castplayer = self.castplayers[player_id] + await asyncio.to_thread(castplayer.cc.set_volume_muted, muted) + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + castplayer = self.castplayers[player_id] + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): + media.uri = media.uri.replace(".flac", ".mp3") + queuedata = { + "type": "LOAD", + "media": self._create_cc_media_item(media), + } + # make sure that our media controller app is launched + await self._launch_app(castplayer) + # send queue info to the CC + media_controller = castplayer.cc.media_controller + await asyncio.to_thread(media_controller.send_message, data=queuedata, inc_session_id=True) + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next item on the player.""" + castplayer = self.castplayers[player_id] + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): + media.uri = media.uri.replace(".flac", ".mp3") + next_item_id = None + 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 + if not cur_item_found: + continue + next_item_id = item["itemId"] + # check if the next queue item isn't already queued + if item.get("media", {}).get("customData", {}).get("uri") == media.uri: + return + queuedata = { + "type": "QUEUE_INSERT", + "insertBefore": next_item_id, + "items": [ + { + "autoplay": True, + "startTime": 0, + "preloadTime": 0, + "media": self._create_cc_media_item(media), + } + ], + } + media_controller = castplayer.cc.media_controller + queuedata["mediaSessionId"] = media_controller.status.media_session_id + self.mass.create_task(media_controller.send_message, data=queuedata, inc_session_id=True) + self.logger.debug( + "Enqued next track (%s) to player %s", + media.title or media.uri, + castplayer.player.display_name, + ) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + castplayer = self.castplayers[player_id] + # only update status of media controller if player is on + if not castplayer.player.powered: + return + if not castplayer.cc.media_controller.is_active: + return + try: + now = time.time() + if (now - castplayer.last_poll) >= 60: + castplayer.last_poll = now + await asyncio.to_thread(castplayer.cc.media_controller.update_status) + await self.update_flow_metadata(castplayer) + except ConnectionResetError as err: + raise PlayerUnavailableError from err + + ### Discovery callbacks + + def _on_chromecast_discovered(self, uuid, _) -> None: + """Handle Chromecast discovered callback.""" + if self.mass.closing: + return + + with self._discover_lock: + disc_info: CastInfo = self.browser.devices[uuid] + + if disc_info.uuid is None: + self.logger.error("Discovered chromecast without uuid %s", disc_info) + return + + player_id = str(disc_info.uuid) + + enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True) + if not enabled: + self.logger.debug("Ignoring disabled player: %s", player_id) + return + + self.logger.debug("Discovered new or updated chromecast %s", disc_info) + + castplayer = self.castplayers.get(player_id) + if castplayer: + # if player was already added, the player will take care of reconnects itself. + castplayer.cast_info.update(disc_info) + self.mass.loop.call_soon_threadsafe(self.mass.players.update, player_id) + return + # new player discovered + cast_info = ChromecastInfo.from_cast_info(disc_info) + cast_info.fill_out_missing_chromecast_info(self.mass.aiozc.zeroconf) + if cast_info.is_dynamic_group: + self.logger.debug("Discovered a dynamic cast group which will be ignored.") + return + if cast_info.is_multichannel_child: + self.logger.debug( + "Discovered a passive (multichannel) endpoint which will be ignored." + ) + return + + # Disable TV's by default + # (can be enabled manually by the user) + enabled_by_default = True + for exclude in ("tv", "/12", "PUS", "OLED"): + if exclude.lower() in cast_info.friendly_name.lower(): + enabled_by_default = False + + if cast_info.is_audio_group and cast_info.is_multichannel_group: + player_type = PlayerType.STEREO_PAIR + elif cast_info.is_audio_group: + player_type = PlayerType.GROUP + else: + player_type = PlayerType.PLAYER + # Instantiate chromecast object + castplayer = CastPlayer( + player_id, + cast_info=cast_info, + cc=pychromecast.get_chromecast_from_cast_info( + disc_info, + self.mass.aiozc.zeroconf, + ), + player=Player( + player_id=player_id, + provider=self.instance_id, + type=player_type, + name=cast_info.friendly_name, + available=False, + powered=False, + device_info=DeviceInfo( + model=cast_info.model_name, + address=f"{cast_info.host}:{cast_info.port}", + manufacturer=cast_info.manufacturer, + ), + supported_features=( + PlayerFeature.POWER, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.VOLUME_SET, + PlayerFeature.PAUSE, + PlayerFeature.NEXT_PREVIOUS, + PlayerFeature.ENQUEUE, + ), + enabled_by_default=enabled_by_default, + needs_poll=True, + ), + ) + self.castplayers[player_id] = castplayer + + castplayer.status_listener = CastStatusListener(self, castplayer, self.mz_mgr) + if castplayer.player.type == PlayerType.GROUP: + mz_controller = MultizoneController(cast_info.uuid) + castplayer.cc.register_handler(mz_controller) + castplayer.mz_controller = mz_controller + + castplayer.cc.start() + asyncio.run_coroutine_threadsafe( + self.mass.players.register_or_update(castplayer.player), loop=self.mass.loop + ) + + def _on_chromecast_removed(self, uuid, service, cast_info) -> None: + """Handle zeroconf discovery of a removed Chromecast.""" + player_id = str(service[1]) + friendly_name = service[3] + self.logger.debug("Chromecast removed: %s - %s", friendly_name, player_id) + # we ignore this event completely as the Chromecast socket client handles this itself + + ### Callbacks from Chromecast Statuslistener + + def on_new_cast_status(self, castplayer: CastPlayer, status: CastStatus) -> None: + """Handle updated CastStatus.""" + if status is None: + return # guard + self.logger.log( + VERBOSE_LOG_LEVEL, + "Received cast status for %s - app_id: %s - volume: %s", + castplayer.player.display_name, + status.app_id, + status.volume_level, + ) + # handle stereo pairs + if castplayer.cast_info.is_multichannel_group: + castplayer.player.type = PlayerType.STEREO_PAIR + castplayer.player.group_childs = set() + # handle cast groups + if castplayer.cast_info.is_audio_group and not castplayer.cast_info.is_multichannel_group: + castplayer.player.type = PlayerType.GROUP + castplayer.player.group_childs = { + str(UUID(x)) for x in castplayer.mz_controller.members + } + castplayer.player.supported_features = ( + PlayerFeature.POWER, + PlayerFeature.VOLUME_SET, + PlayerFeature.PAUSE, + PlayerFeature.ENQUEUE, + ) + + # update player status + castplayer.player.name = castplayer.cast_info.friendly_name + castplayer.player.volume_level = int(status.volume_level * 100) + castplayer.player.volume_muted = status.volume_muted + new_powered = ( + castplayer.cc.app_id is not None and castplayer.cc.app_id != pychromecast.IDLE_APP_ID + ) + if ( + castplayer.player.powered + and not new_powered + and castplayer.player.type == PlayerType.GROUP + ): + # group is being powered off, update group childs + for child_id in castplayer.player.group_childs: + if child := self.castplayers.get(child_id): + child.player.powered = False + child.player.active_group = None + child.player.active_source = None + castplayer.player.powered = new_powered + # send update to player manager + self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) + + def on_new_media_status(self, castplayer: CastPlayer, status: MediaStatus) -> None: + """Handle updated MediaStatus.""" + self.logger.log( + VERBOSE_LOG_LEVEL, + "Received media status for %s update: %s", + castplayer.player.display_name, + status.player_state, + ) + # handle castplayer playing from a group + group_player: CastPlayer | None = None + if castplayer.active_group is not None: + if not (group_player := self.castplayers.get(castplayer.active_group)): + return + status = group_player.cc.media_controller.status + + # 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.elapsed_time = status.current_time + + # active source + if group_player: + castplayer.player.active_source = ( + group_player.player.active_source or group_player.player.player_id + ) + castplayer.player.active_group = ( + group_player.player.active_group or group_player.player.player_id + ) + elif castplayer.cc.app_id == MASS_APP_ID: + castplayer.player.active_source = castplayer.player_id + else: + castplayer.player.active_source = castplayer.cc.app_display_name + + if status.content_id and not status.player_is_idle: + castplayer.player.current_media = PlayerMedia( + uri=status.content_id, + title=status.title, + artist=status.artist, + album=status.album_name, + image_url=status.images[0].url if status.images else None, + duration=status.duration, + media_type=MediaType.TRACK, + ) + else: + castplayer.player.current_media = None + + # weird workaround which is needed for multichannel group childs + # (e.g. a stereo pair within a cast group) + # where it does not receive updates from the group, + # so we need to update the group child(s) manually + if castplayer.player.type == PlayerType.GROUP and castplayer.player.powered: + for child_id in castplayer.player.group_childs: + if child := self.castplayers.get(child_id): + if not child.cast_info.is_multichannel_group: + continue + child.player.state = castplayer.player.state + child.player.current_media = castplayer.player.current_media + child.player.elapsed_time = castplayer.player.elapsed_time + child.player.elapsed_time_last_updated = ( + castplayer.player.elapsed_time_last_updated + ) + child.player.active_source = castplayer.player.active_source + child.player.active_group = castplayer.player.active_group + + self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) + + def on_new_connection_status(self, castplayer: CastPlayer, status: ConnectionStatus) -> None: + """Handle updated ConnectionStatus.""" + self.logger.log( + VERBOSE_LOG_LEVEL, + "Received connection status update for %s - status: %s", + castplayer.player.display_name, + status.status, + ) + + if status.status == CONNECTION_STATUS_DISCONNECTED: + castplayer.player.available = False + self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) + return + + new_available = status.status == CONNECTION_STATUS_CONNECTED + if new_available != castplayer.player.available: + self.logger.debug( + "[%s] Cast device availability changed: %s", + castplayer.cast_info.friendly_name, + status.status, + ) + castplayer.player.available = new_available + castplayer.player.device_info = DeviceInfo( + model=castplayer.cast_info.model_name, + address=f"{castplayer.cast_info.host}:{castplayer.cast_info.port}", + manufacturer=castplayer.cast_info.manufacturer, + ) + self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) + if new_available and castplayer.player.type == PlayerType.PLAYER: + # Poll current group status + for group_uuid in self.mz_mgr.get_multizone_memberships(castplayer.cast_info.uuid): + group_media_controller = self.mz_mgr.get_multizone_mediacontroller(group_uuid) + if not group_media_controller: + continue + + ### Helpers / utils + + async def _launch_app(self, castplayer: CastPlayer, app_id: str = MASS_APP_ID) -> None: + """Launch the default Media Receiver App on a Chromecast.""" + event = asyncio.Event() + + if castplayer.cc.app_id == app_id: + return # already active + + def launched_callback(success: bool, response: dict[str, Any] | None) -> None: + self.mass.loop.call_soon_threadsafe(event.set) + + def launch() -> None: + # Quit the previous app before starting splash screen or media player + if castplayer.cc.app_id is not None: + castplayer.cc.quit_app() + self.logger.debug("Launching App %s.", app_id) + castplayer.cc.socket_client.receiver_controller.launch_app( + app_id, + force_launch=True, + callback_function=launched_callback, + ) + + await self.mass.loop.run_in_executor(None, launch) + await event.wait() + + async def _disconnect_chromecast(self, castplayer: CastPlayer) -> None: + """Disconnect Chromecast object if it is set.""" + self.logger.debug("Disconnecting from chromecast socket %s", castplayer.player.display_name) + await self.mass.loop.run_in_executor(None, castplayer.cc.disconnect, 10) + castplayer.mz_controller = None + castplayer.status_listener.invalidate() + castplayer.status_listener = None + self.castplayers.pop(castplayer.player_id, None) + + def _create_cc_media_item(self, media: PlayerMedia) -> dict[str, Any]: + """Create CC media item from MA PlayerMedia.""" + if media.media_type == MediaType.TRACK: + stream_type = STREAM_TYPE_BUFFERED + else: + stream_type = STREAM_TYPE_LIVE + metadata = { + "metadataType": 3, + "albumName": media.album or "", + "songName": media.title or "", + "artist": media.artist or "", + "title": media.title or "", + "images": [{"url": media.image_url}] if media.image_url else None, + } + return { + "contentId": media.uri, + "customData": { + "uri": media.uri, + "queue_item_id": media.uri, + "deviceName": "Music Assistant", + }, + "contentType": "audio/flac", + "streamType": stream_type, + "metadata": metadata, + "duration": media.duration, + } + + async def update_flow_metadata(self, castplayer: CastPlayer) -> None: + """Update the metadata of a cast player running the flow stream.""" + if not castplayer.player.powered: + castplayer.player.poll_interval = 300 + return + if not castplayer.cc.media_controller.status.player_is_playing: + return + if castplayer.active_group: + return + if castplayer.player.state != PlayerState.PLAYING: + return + if castplayer.player.announcement_in_progress: + return + if not (queue := self.mass.player_queues.get_active_queue(castplayer.player_id)): + return + if not (current_item := queue.current_item): + return + if not (queue.flow_mode or current_item.media_type == MediaType.RADIO): + return + castplayer.player.poll_interval = 10 + media_controller = castplayer.cc.media_controller + # update metadata of current item chromecast + if media_controller.status.media_custom_data["queue_item_id"] != current_item.queue_item_id: + image_url = ( + self.mass.metadata.get_image_url(current_item.image) + if current_item.image + else MASS_LOGO_ONLINE + ) + if (streamdetails := current_item.streamdetails) and streamdetails.stream_title: + album = current_item.media_item.name + if " - " in streamdetails.stream_title: + artist, title = streamdetails.stream_title.split(" - ", 1) + else: + artist = "" + title = streamdetails.stream_title + elif media_item := current_item.media_item: + album = _album.name if (_album := getattr(media_item, "album", None)) else "" + artist = getattr(media_item, "artist_str", "") + title = media_item.name + else: + album = "" + artist = "" + title = current_item.name + flow_meta_checksum = title + image_url + if castplayer.flow_meta_checksum == flow_meta_checksum: + return + castplayer.flow_meta_checksum = flow_meta_checksum + queuedata = { + "type": "PLAY", + "mediaSessionId": media_controller.status.media_session_id, + "customData": { + "metadata": { + "metadataType": 3, + "albumName": album, + "songName": title, + "artist": artist, + "title": title, + "images": [{"url": image_url}], + } + }, + } + self.mass.create_task( + media_controller.send_message, data=queuedata, inc_session_id=True + ) + + if len(getattr(media_controller.status, "items", [])) < 2: + # In flow mode, all queue tracks are sent to the player as continuous stream. + # add a special 'command' item to the queue + # this allows for on-player next buttons/commands to still work + cmd_next_url = self.mass.streams.get_command_url(queue.queue_id, "next") + msg = { + "type": "QUEUE_INSERT", + "mediaSessionId": media_controller.status.media_session_id, + "items": [ + { + "media": { + "contentId": cmd_next_url, + "customData": { + "uri": cmd_next_url, + "queue_item_id": cmd_next_url, + "deviceName": "Music Assistant", + }, + "contentType": "audio/flac", + "streamType": STREAM_TYPE_LIVE, + "metadata": {}, + }, + "autoplay": True, + "startTime": 0, + "preloadTime": 0, + } + ], + } + self.mass.create_task(media_controller.send_message, data=msg, inc_session_id=True) diff --git a/music_assistant/providers/chromecast/helpers.py b/music_assistant/providers/chromecast/helpers.py new file mode 100644 index 00000000..098062c2 --- /dev/null +++ b/music_assistant/providers/chromecast/helpers.py @@ -0,0 +1,232 @@ +"""Helpers to deal with Cast devices.""" + +from __future__ import annotations + +import urllib.error +from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING, Self +from uuid import UUID + +from pychromecast import dial +from pychromecast.const import CAST_TYPE_GROUP + +from music_assistant.constants import VERBOSE_LOG_LEVEL + +if TYPE_CHECKING: + from pychromecast.controllers.media import MediaStatus + from pychromecast.controllers.multizone import MultizoneManager + from pychromecast.controllers.receiver import CastStatus + from pychromecast.models import CastInfo + from pychromecast.socket_client import ConnectionStatus + from zeroconf import ServiceInfo, Zeroconf + + from . import CastPlayer, ChromecastProvider + +DEFAULT_PORT = 8009 + + +@dataclass +class ChromecastInfo: + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + services: set + uuid: UUID + model_name: str + friendly_name: str + host: str + port: int + cast_type: str | None = None + manufacturer: str | None = None + is_dynamic_group: bool | None = None + is_multichannel_group: bool = False # group created for e.g. stereo pair + is_multichannel_child: bool = False # speaker that is part of multichannel setup + + @property + def is_audio_group(self) -> bool: + """Return if the cast is an audio group.""" + return self.cast_type == CAST_TYPE_GROUP + + @classmethod + def from_cast_info(cls: Self, cast_info: CastInfo) -> Self: + """Instantiate ChromecastInfo from CastInfo.""" + return cls(**asdict(cast_info)) + + def update(self, cast_info: CastInfo) -> None: + """Update ChromecastInfo from CastInfo.""" + for key, value in asdict(cast_info).items(): + if not value: + continue + setattr(self, key, value) + + def fill_out_missing_chromecast_info(self, zconf: Zeroconf) -> None: + """ + Return a new ChromecastInfo object with missing attributes filled in. + + Uses blocking HTTP / HTTPS. + """ + if self.cast_type is None or self.manufacturer is None: + # Manufacturer and cast type is not available in mDNS data, + # get it over HTTP + cast_info = dial.get_cast_type( + self, + zconf=zconf, + ) + self.cast_type = cast_info.cast_type + self.manufacturer = cast_info.manufacturer + + # Fill out missing group information via HTTP API. + dynamic_groups, multichannel_groups = get_multizone_info(self.services, zconf) + self.is_dynamic_group = self.uuid in dynamic_groups + if self.uuid in multichannel_groups: + self.is_multichannel_group = True + elif multichannel_groups: + self.is_multichannel_child = True + + +def get_multizone_info(services: list[ServiceInfo], zconf: Zeroconf, timeout=30): + """Get multizone info from eureka endpoint.""" + dynamic_groups: set[str] = set() + multichannel_groups: set[str] = set() + try: + _, status = dial._get_status( + services, + zconf, + "/setup/eureka_info?params=multizone", + True, + timeout, + None, + ) + if "multizone" in status and "dynamic_groups" in status["multizone"]: + for group in status["multizone"]["dynamic_groups"]: + if udn := group.get("uuid"): + uuid = UUID(udn.replace("-", "")) + dynamic_groups.add(uuid) + + if "multizone" in status and "groups" in status["multizone"]: + for group in status["multizone"]["groups"]: + if group["multichannel_group"] and (udn := group.get("uuid")): + uuid = UUID(udn.replace("-", "")) + multichannel_groups.add(uuid) + except (urllib.error.HTTPError, urllib.error.URLError, OSError, ValueError): + pass + return (dynamic_groups, multichannel_groups) + + +class CastStatusListener: + """ + Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__( + self, + prov: ChromecastProvider, + castplayer: CastPlayer, + mz_mgr: MultizoneManager, + mz_only=False, + ) -> None: + """Initialize the status listener.""" + self.prov = prov + self.castplayer = castplayer + self._uuid = castplayer.cc.uuid + self._valid = True + self._mz_mgr = mz_mgr + + if self.castplayer.cast_info.is_audio_group: + self._mz_mgr.add_multizone(castplayer.cc) + if mz_only: + return + + castplayer.cc.register_status_listener(self) + castplayer.cc.socket_client.media_controller.register_status_listener(self) + castplayer.cc.register_connection_listener(self) + if not self.castplayer.cast_info.is_audio_group: + self._mz_mgr.register_listener(castplayer.cc.uuid, self) + + def new_cast_status(self, status: CastStatus) -> None: + """Handle updated CastStatus.""" + if not self._valid: + return + self.prov.on_new_cast_status(self.castplayer, status) + + def new_media_status(self, status: MediaStatus) -> None: + """Handle updated MediaStatus.""" + if not self._valid: + return + self.prov.on_new_media_status(self.castplayer, status) + + def new_connection_status(self, status: ConnectionStatus) -> None: + """Handle updated ConnectionStatus.""" + if not self._valid: + return + self.prov.on_new_connection_status(self.castplayer, status) + + def added_to_multizone(self, group_uuid) -> None: + """Handle the cast added to a group.""" + self.prov.logger.debug( + "%s is added to multizone: %s", self.castplayer.player.display_name, group_uuid + ) + self.new_cast_status(self.castplayer.cc.status) + + def removed_from_multizone(self, group_uuid) -> None: + """Handle the cast removed from a group.""" + if not self._valid: + return + if group_uuid == self.castplayer.player.active_source: + self.castplayer.player.active_source = None + self.prov.logger.debug( + "%s is removed from multizone: %s", self.castplayer.player.display_name, group_uuid + ) + self.new_cast_status(self.castplayer.cc.status) + + def multizone_new_cast_status(self, group_uuid, cast_status) -> None: + """Handle reception of a new CastStatus for a group.""" + if group_player := self.prov.castplayers.get(group_uuid): + if group_player.cc.media_controller.is_active: + self.castplayer.active_group = group_uuid + elif group_uuid == self.castplayer.active_group: + self.castplayer.active_group = None + + self.prov.logger.log( + VERBOSE_LOG_LEVEL, + "%s got new cast status for group: %s", + self.castplayer.player.display_name, + group_uuid, + ) + self.new_cast_status(self.castplayer.cc.status) + + def multizone_new_media_status(self, group_uuid, media_status) -> None: + """Handle reception of a new MediaStatus for a group.""" + if not self._valid: + return + self.prov.logger.log( + VERBOSE_LOG_LEVEL, + "%s got new media_status for group: %s", + self.castplayer.player.display_name, + group_uuid, + ) + self.prov.on_new_media_status(self.castplayer, media_status) + + def load_media_failed(self, queue_item_id, error_code) -> None: + """Call when media failed to load.""" + self.prov.logger.warning( + "Load media failed: %s - error code: %s", queue_item_id, error_code + ) + + def invalidate(self) -> None: + """ + Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + if self.castplayer.cast_info.is_audio_group: + self._mz_mgr.remove_multizone(self._uuid) + else: + self._mz_mgr.deregister_listener(self._uuid, self) + self._valid = False diff --git a/music_assistant/providers/chromecast/manifest.json b/music_assistant/providers/chromecast/manifest.json new file mode 100644 index 00000000..6bcee60c --- /dev/null +++ b/music_assistant/providers/chromecast/manifest.json @@ -0,0 +1,19 @@ +{ + "type": "player", + "domain": "chromecast", + "name": "Chromecast", + "description": "Support for Chromecast based players.", + "codeowners": [ + "@music-assistant" + ], + "requirements": [ + "PyChromecast==14.0.4" + ], + "documentation": "https://music-assistant.io/player-support/google-cast/", + "multi_instance": false, + "builtin": false, + "icon": "cast", + "mdns_discovery": [ + "_googlecast._tcp.local." + ] +} diff --git a/music_assistant/providers/deezer/__init__.py b/music_assistant/providers/deezer/__init__.py new file mode 100644 index 00000000..eb79c490 --- /dev/null +++ b/music_assistant/providers/deezer/__init__.py @@ -0,0 +1,765 @@ +"""Deezer music provider support for MusicAssistant.""" + +import hashlib +import uuid +from asyncio import TaskGroup +from collections.abc import AsyncGenerator +from dataclasses import dataclass +from math import ceil + +import deezer +from aiohttp import ClientSession, ClientTimeout +from Crypto.Cipher import Blowfish +from deezer import exceptions as deezer_exceptions +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ( + AlbumType, + ConfigEntryType, + ContentType, + ExternalID, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import LoginFailed +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + ItemMapping, + MediaItemImage, + MediaItemMetadata, + MediaItemType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.provider import ProviderManifest +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant import MusicAssistant +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.datetime import utc_timestamp +from music_assistant.models import ProviderInstanceType +from music_assistant.models.music_provider import MusicProvider + +from .gw_client import GWClient + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.ALBUM_METADATA, + ProviderFeature.TRACK_METADATA, + ProviderFeature.ARTIST_METADATA, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ProviderFeature.PLAYLIST_CREATE, + ProviderFeature.RECOMMENDATIONS, + ProviderFeature.SIMILAR_TRACKS, +) + + +@dataclass +class DeezerCredentials: + """Class for storing credentials.""" + + app_id: int + app_secret: str + access_token: str + + +CONF_ACCESS_TOKEN = "access_token" +CONF_ARL_TOKEN = "arl_token" +CONF_ACTION_AUTH = "auth" +DEEZER_AUTH_URL = "https://connect.deezer.com/oauth/auth.php" +RELAY_URL = "https://deezer.oauth.jonathanbangert.com/" +DEEZER_PERMS = "basic_access,email,offline_access,manage_library,\ +manage_community,delete_library,listening_history" +DEEZER_APP_ID = app_var(6) +DEEZER_APP_SECRET = app_var(7) + + +async def get_access_token( + app_id: str, app_secret: str, code: str, http_session: ClientSession +) -> str: + """Update the access_token.""" + response = await http_session.post( + "https://connect.deezer.com/oauth/access_token.php", + params={"code": code, "app_id": app_id, "secret": app_secret}, + ssl=False, + ) + if response.status != 200: + msg = f"HTTP Error {response.status}: {response.reason}" + raise ConnectionError(msg) + response_text = await response.text() + try: + return response_text.split("=")[1].split("&")[0] + except Exception as error: + msg = "Invalid auth code" + raise LoginFailed(msg) from error + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return DeezerProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """Return Config entries to setup this provider.""" + # Action is to launch oauth flow + if action == CONF_ACTION_AUTH: + # Use the AuthenticationHelper to authenticate + async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: # type: ignore + url = f"{DEEZER_AUTH_URL}?app_id={DEEZER_APP_ID}&redirect_uri={RELAY_URL}\ +&perms={DEEZER_PERMS}&state={auth_helper.callback_url}" + code = (await auth_helper.authenticate(url))["code"] + values[CONF_ACCESS_TOKEN] = await get_access_token( # type: ignore + DEEZER_APP_ID, DEEZER_APP_SECRET, code, mass.http_session + ) + + return ( + ConfigEntry( + key=CONF_ACCESS_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Access token", + required=True, + action=CONF_ACTION_AUTH, + description="You need to authenticate on Deezer.", + action_label="Authenticate with Deezer", + value=values.get(CONF_ACCESS_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_ARL_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Arl token", + required=True, + description="See https://www.dumpmedia.com/deezplus/deezer-arl.html", + value=values.get(CONF_ARL_TOKEN) if values else None, + ), + ) + + +class DeezerProvider(MusicProvider): + """Deezer provider support.""" + + client: deezer.Client + gw_client: GWClient + credentials: DeezerCredentials + user: deezer.User + + async def handle_async_init(self) -> None: + """Handle async init of the Deezer provider.""" + self.credentials = DeezerCredentials( + app_id=DEEZER_APP_ID, + app_secret=DEEZER_APP_SECRET, + access_token=self.config.get_value(CONF_ACCESS_TOKEN), # type: ignore + ) + + self.client = deezer.Client( + app_id=self.credentials.app_id, + app_secret=self.credentials.app_secret, + access_token=self.credentials.access_token, + ) + + self.user = await self.client.get_user() + + self.gw_client = GWClient( + self.mass.http_session, + self.config.get_value(CONF_ACCESS_TOKEN), + self.config.get_value(CONF_ARL_TOKEN), + ) + await self.gw_client.setup() + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def search( + self, search_query: str, media_types=list[MediaType], limit: int = 5 + ) -> SearchResults: + """Perform search on music provider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + """ + # Create a task for each media_type + tasks = {} + + async with TaskGroup() as taskgroup: + for media_type in media_types: + if media_type == MediaType.TRACK: + tasks[MediaType.TRACK] = taskgroup.create_task( + self.search_and_parse_tracks( + query=search_query, + limit=limit, + user_country=self.gw_client.user_country, + ) + ) + elif media_type == MediaType.ARTIST: + tasks[MediaType.ARTIST] = taskgroup.create_task( + self.search_and_parse_artists(query=search_query, limit=limit) + ) + elif media_type == MediaType.ALBUM: + tasks[MediaType.ALBUM] = taskgroup.create_task( + self.search_and_parse_albums(query=search_query, limit=limit) + ) + elif media_type == MediaType.PLAYLIST: + tasks[MediaType.PLAYLIST] = taskgroup.create_task( + self.search_and_parse_playlists(query=search_query, limit=limit) + ) + + results = SearchResults() + + for media_type, task in tasks.items(): + if media_type == MediaType.ARTIST: + results.artists = task.result() + elif media_type == MediaType.ALBUM: + results.albums = task.result() + elif media_type == MediaType.TRACK: + results.tracks = task.result() + elif media_type == MediaType.PLAYLIST: + results.playlists = task.result() + + return results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Deezer.""" + async for artist in await self.client.get_user_artists(): + yield self.parse_artist(artist=artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Deezer.""" + async for album in await self.client.get_user_albums(): + yield self.parse_album(album=album) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from Deezer.""" + async for playlist in await self.user.get_playlists(): + yield self.parse_playlist(playlist=playlist) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve all library tracks from Deezer.""" + async for track in await self.client.get_user_tracks(): + yield self.parse_track(track=track, user_country=self.gw_client.user_country) + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + try: + return self.parse_artist( + artist=await self.client.get_artist(artist_id=int(prov_artist_id)) + ) + except deezer_exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting artist: %s", error) + + async def get_album(self, prov_album_id: str) -> Album: + """Get full album details by id.""" + try: + return self.parse_album(album=await self.client.get_album(album_id=int(prov_album_id))) + except deezer_exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting album: %s", error) + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get full playlist details by id.""" + try: + return self.parse_playlist( + playlist=await self.client.get_playlist(playlist_id=int(prov_playlist_id)), + ) + except deezer_exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting playlist: %s", error) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + try: + return self.parse_track( + track=await self.client.get_track(track_id=int(prov_track_id)), + user_country=self.gw_client.user_country, + ) + except deezer_exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting track: %s", error) + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get all tracks in an album.""" + album = await self.client.get_album(album_id=int(prov_album_id)) + return [ + self.parse_track( + track=deezer_track, + user_country=self.gw_client.user_country, + # TODO: doesn't Deezer have disc and track number in the api ? + position=0, + ) + for deezer_track in await album.get_tracks() + ] + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # paging not supported, we always return the whole list at once + return [] + # TODO: access the underlying paging on the deezer api (if possible)) + playlist = await self.client.get_playlist(int(prov_playlist_id)) + playlist_tracks = await playlist.get_tracks() + for index, deezer_track in enumerate(playlist_tracks, 1): + result.append( + self.parse_track( + track=deezer_track, + user_country=self.gw_client.user_country, + position=index, + ) + ) + return result + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get albums by an artist.""" + artist = await self.client.get_artist(artist_id=int(prov_artist_id)) + return [self.parse_album(album=album) async for album in await artist.get_albums()] + + async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: + """Get top 50 tracks of an artist.""" + artist = await self.client.get_artist(artist_id=int(prov_artist_id)) + return [ + self.parse_track(track=track, user_country=self.gw_client.user_country) + async for track in await artist.get_top(limit=50) + ] + + async def library_add(self, item: MediaItemType) -> bool: + """Add an item to the provider's library/favorites.""" + result = False + if item.media_type == MediaType.ARTIST: + result = await self.client.add_user_artist( + artist_id=int(item.item_id), + ) + elif item.media_type == MediaType.ALBUM: + result = await self.client.add_user_album( + album_id=int(item.item_id), + ) + elif item.media_type == MediaType.TRACK: + result = await self.client.add_user_track( + track_id=int(item.item_id), + ) + elif item.media_type == MediaType.PLAYLIST: + result = await self.client.add_user_playlist( + playlist_id=int(item.item_id), + ) + else: + raise NotImplementedError + return result + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove an item from the provider's library/favorites.""" + result = False + if media_type == MediaType.ARTIST: + result = await self.client.remove_user_artist( + artist_id=int(prov_item_id), + ) + elif media_type == MediaType.ALBUM: + result = await self.client.remove_user_album( + album_id=int(prov_item_id), + ) + elif media_type == MediaType.TRACK: + result = await self.client.remove_user_track( + track_id=int(prov_item_id), + ) + elif media_type == MediaType.PLAYLIST: + result = await self.client.remove_user_playlist( + playlist_id=int(prov_item_id), + ) + else: + raise NotImplementedError + return result + + async def recommendations(self) -> list[Track]: + """Get deezer's recommendations.""" + return [ + self.parse_track(track=track, user_country=self.gw_client.user_country) + for track in await self.client.get_user_recommended_tracks() + ] + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + playlist = await self.client.get_playlist(int(prov_playlist_id)) + await playlist.add_tracks(tracks=[int(i) for i in prov_track_ids]) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + playlist_track_ids = [] + for track in await self.get_playlist_tracks(prov_playlist_id, 0): + if track.position in positions_to_remove: + playlist_track_ids.append(int(track.item_id)) + if len(playlist_track_ids) == len(positions_to_remove): + break + playlist = await self.client.get_playlist(int(prov_playlist_id)) + await playlist.delete_tracks(playlist_track_ids) + + async def create_playlist(self, name: str) -> Playlist: + """Create a new playlist on provider with given name.""" + playlist_id = await self.client.create_playlist(playlist_name=name) + playlist = await self.client.get_playlist(playlist_id) + return self.parse_playlist(playlist=playlist) + + async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + endpoint = "song.getSearchTrackMix" + tracks = (await self.gw_client._gw_api_call(endpoint, args={"SNG_ID": prov_track_id}))[ + "results" + ]["data"][:limit] + return [await self.get_track(track["SNG_ID"]) for track in tracks] + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + url_details, song_data = await self.gw_client.get_deezer_track_urls(item_id) + url = url_details["sources"][0]["url"] + return StreamDetails( + item_id=item_id, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(url_details["format"].split("_")[0]) + ), + stream_type=StreamType.CUSTOM, + duration=int(song_data["DURATION"]), + data={"url": url, "format": url_details["format"]}, + size=int(song_data[f"FILESIZE_{url_details['format']}"]), + ) + + async def get_audio_stream( + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """Return the audio stream for the provider item.""" + blowfish_key = self.get_blowfish_key(streamdetails.item_id) + chunk_index = 0 + timeout = ClientTimeout(total=0, connect=30, sock_read=600) + headers = {} + # if seek_position and streamdetails.size: + # chunk_count = ceil(streamdetails.size / 2048) + # chunk_index = int(chunk_count / streamdetails.duration) * seek_position + # skip_bytes = chunk_index * 2048 + # headers["Range"] = f"bytes={skip_bytes}-" + + # NOTE: Seek with using the Range header is not working properly + # causing malformed audio so this is a temporary patch + # by just skipping chunks + if seek_position and streamdetails.size: + chunk_count = ceil(streamdetails.size / 2048) + skip_chunks = int(chunk_count / streamdetails.duration) * seek_position + else: + skip_chunks = 0 + + buffer = bytearray() + streamdetails.data["start_ts"] = utc_timestamp() + streamdetails.data["stream_id"] = uuid.uuid1() + self.mass.create_task(self.gw_client.log_listen(next_track=streamdetails.item_id)) + async with self.mass.http_session.get( + streamdetails.data["url"], headers=headers, timeout=timeout + ) as resp: + async for chunk in resp.content.iter_chunked(2048): + buffer += chunk + if len(buffer) >= 2048: + if chunk_index >= skip_chunks or chunk_index == 0: + if chunk_index % 3 > 0: + yield bytes(buffer[:2048]) + else: + yield self.decrypt_chunk(bytes(buffer[:2048]), blowfish_key) + + chunk_index += 1 + del buffer[:2048] + yield bytes(buffer) + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + await self.gw_client.log_listen(last_track=streamdetails) + + ### PARSING METADATA FUNCTIONS ### + + def parse_metadata_track(self, track: deezer.Track) -> MediaItemMetadata: + """Parse the track metadata.""" + metadata = MediaItemMetadata() + if hasattr(track, "preview"): + metadata.preview = track.preview + if hasattr(track, "explicit_lyrics"): + metadata.explicit = track.explicit_lyrics + if hasattr(track, "duration"): + metadata.duration = track.duration + if hasattr(track, "rank"): + metadata.popularity = track.rank + if hasattr(track, "release_date"): + metadata.release_date = track.release_date + if hasattr(track, "album") and hasattr(track.album, "cover_big"): + metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=track.album.cover_big, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + return metadata + + def parse_metadata_album(self, album: deezer.Album) -> MediaItemMetadata: + """Parse the album metadata.""" + return MediaItemMetadata( + explicit=album.explicit_lyrics, + images=[ + MediaItemImage( + type=ImageType.THUMB, + path=album.cover_big, + provider=self.lookup_key, + remotely_accessible=True, + ) + ], + ) + + def parse_metadata_artist(self, artist: deezer.Artist) -> MediaItemMetadata: + """Parse the artist metadata.""" + return MediaItemMetadata( + images=[ + MediaItemImage( + type=ImageType.THUMB, + path=artist.picture_big, + provider=self.lookup_key, + remotely_accessible=True, + ) + ], + ) + + ### PARSING FUNCTIONS ### + def parse_artist(self, artist: deezer.Artist) -> Artist: + """Parse the deezer-python artist to a Music Assistant artist.""" + return Artist( + item_id=str(artist.id), + provider=self.domain, + name=artist.name, + media_type=MediaType.ARTIST, + provider_mappings={ + ProviderMapping( + item_id=str(artist.id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=artist.link, + ) + }, + metadata=self.parse_metadata_artist(artist=artist), + ) + + def parse_album(self, album: deezer.Album) -> Album: + """Parse the deezer-python album to a Music Assistant album.""" + return Album( + album_type=AlbumType(album.type), + item_id=str(album.id), + provider=self.domain, + name=album.title, + artists=[ + ItemMapping( + media_type=MediaType.ARTIST, + item_id=str(album.artist.id), + provider=self.instance_id, + name=album.artist.name, + ) + ], + media_type=MediaType.ALBUM, + provider_mappings={ + ProviderMapping( + item_id=str(album.id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=album.link, + ) + }, + metadata=self.parse_metadata_album(album=album), + ) + + def parse_playlist(self, playlist: deezer.Playlist) -> Playlist: + """Parse the deezer-python playlist to a Music Assistant playlist.""" + creator = self.get_playlist_creator(playlist) + return Playlist( + item_id=str(playlist.id), + provider=self.domain, + name=playlist.title, + media_type=MediaType.PLAYLIST, + provider_mappings={ + ProviderMapping( + item_id=str(playlist.id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=playlist.link, + ) + }, + metadata=MediaItemMetadata( + images=[ + MediaItemImage( + type=ImageType.THUMB, + path=playlist.picture_big, + provider=self.lookup_key, + remotely_accessible=True, + ) + ], + ), + is_editable=creator.id == self.user.id, + owner=creator.name, + cache_checksum=playlist.checksum, + ) + + def get_playlist_creator(self, playlist: deezer.Playlist): + """On playlists, the creator is called creator, elsewhere it's called user.""" + if hasattr(playlist, "creator"): + return playlist.creator + return playlist.user + + def parse_track(self, track: deezer.Track, user_country: str, position: int = 0) -> Track: + """Parse the deezer-python track to a Music Assistant track.""" + if hasattr(track, "artist"): + artist = ItemMapping( + media_type=MediaType.ARTIST, + item_id=str(getattr(track.artist, "id", f"deezer-{track.artist.name}")), + provider=self.instance_id, + name=track.artist.name, + ) + else: + artist = None + if hasattr(track, "album"): + album = ItemMapping( + media_type=MediaType.ALBUM, + item_id=str(track.album.id), + provider=self.instance_id, + name=track.album.title, + ) + else: + album = None + + item = Track( + item_id=str(track.id), + provider=self.domain, + name=track.title, + sort_name=self.get_short_title(track), + duration=track.duration, + artists=[artist] if artist else [], + album=album, + provider_mappings={ + ProviderMapping( + item_id=str(track.id), + provider_domain=self.domain, + provider_instance=self.instance_id, + available=self.track_available(track=track, user_country=user_country), + url=track.link, + ) + }, + metadata=self.parse_metadata_track(track=track), + track_number=position, + position=position, + disc_number=getattr(track, "disk_number", 0), + ) + if isrc := getattr(track, "isrc", None): + item.external_ids.add((ExternalID.ISRC, isrc)) + return item + + def get_short_title(self, track: deezer.Track): + """Short names only returned, if available.""" + if hasattr(track, "title_short"): + return track.title_short + return track.title + + ### SEARCH AND PARSE FUNCTIONS ### + async def search_and_parse_tracks( + self, query: str, user_country: str, limit: int = 20 + ) -> list[Track]: + """Search for tracks and parse them.""" + deezer_tracks = await self.client.search(query=query, limit=limit) + tracks = [] + for index, track in enumerate(deezer_tracks): + tracks.append(self.parse_track(track, user_country)) + if index == limit: + return tracks + return tracks + + async def search_and_parse_artists(self, query: str, limit: int = 20) -> list[Artist]: + """Search for artists and parse them.""" + deezer_artist = await self.client.search_artists(query=query, limit=limit) + artists = [] + for index, artist in enumerate(deezer_artist): + artists.append(self.parse_artist(artist)) + if index == limit: + return artists + return artists + + async def search_and_parse_albums(self, query: str, limit: int = 20) -> list[Album]: + """Search for album and parse them.""" + deezer_albums = await self.client.search_albums(query=query, limit=limit) + albums = [] + for index, album in enumerate(deezer_albums): + albums.append(self.parse_album(album)) + if index == limit: + return albums + return albums + + async def search_and_parse_playlists(self, query: str, limit: int = 20) -> list[Playlist]: + """Search for playlists and parse them.""" + deezer_playlists = await self.client.search_playlists(query=query, limit=limit) + playlists = [] + for index, playlist in enumerate(deezer_playlists): + playlists.append(self.parse_playlist(playlist)) + if index == limit: + return playlists + return playlists + + ### OTHER FUNCTIONS ### + + async def get_track_content_type(self, gw_client: GWClient, track_id: int): + """Get a tracks contentType.""" + song_data = await gw_client.get_song_data(track_id) + if song_data["results"]["FILESIZE_FLAC"]: + return ContentType.FLAC + + if song_data["results"]["FILESIZE_MP3_320"] or song_data["results"]["FILESIZE_MP3_128"]: + return ContentType.MP3 + + msg = "Unsupported contenttype" + raise NotImplementedError(msg) + + def track_available(self, track: deezer.Track, user_country: str) -> bool: + """Check if a given track is available in the users country.""" + if hasattr(track, "available_countries"): + return user_country in track.available_countries + return True + + def _md5(self, data, data_type="ascii"): + md5sum = hashlib.md5() + md5sum.update(data.encode(data_type)) + return md5sum.hexdigest() + + def get_blowfish_key(self, track_id): + """Get blowfish key to decrypt a chunk of a track.""" + secret = app_var(5) + id_md5 = self._md5(track_id) + return "".join( + chr(ord(id_md5[i]) ^ ord(id_md5[i + 16]) ^ ord(secret[i])) for i in range(16) + ) + + def decrypt_chunk(self, chunk, blowfish_key): + """Decrypt a given chunk using the blow fish key.""" + cipher = Blowfish.new( + blowfish_key.encode("ascii"), + Blowfish.MODE_CBC, + b"\x00\x01\x02\x03\x04\x05\x06\x07", + ) + return cipher.decrypt(chunk) diff --git a/music_assistant/providers/deezer/gw_client.py b/music_assistant/providers/deezer/gw_client.py new file mode 100644 index 00000000..e0e83f75 --- /dev/null +++ b/music_assistant/providers/deezer/gw_client.py @@ -0,0 +1,197 @@ +"""A minimal client for the unofficial gw-API, which deezer is using on their website and app. + +Credits go out to RemixDev (https://gitlab.com/RemixDev) for figuring out, how to get the arl +cookie based on the api_token. +""" + +import datetime +from http.cookies import BaseCookie, Morsel + +from aiohttp import ClientSession +from music_assistant_models.streamdetails import StreamDetails +from yarl import URL + +from music_assistant.helpers.datetime import utc_timestamp + +USER_AGENT_HEADER = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/79.0.3945.130 Safari/537.36" +) + +GW_LIGHT_URL = "https://www.deezer.com/ajax/gw-light.php" + + +class DeezerGWError(BaseException): + """Exception type for GWClient related exceptions.""" + + +class GWClient: + """The GWClient class can be used to perform actions not being of the official API.""" + + _arl_token: str + _api_token: str + _gw_csrf_token: str | None + _license: str | None + _license_expiration_timestamp: int + session: ClientSession + formats: list[dict[str, str]] = [ + {"cipher": "BF_CBC_STRIPE", "format": "MP3_128"}, + ] + user_country: str + + def __init__(self, session: ClientSession, api_token: str, arl_token: str) -> None: + """Provide an aiohttp ClientSession and the deezer api_token.""" + self._api_token = api_token + self._arl_token = arl_token + self.session = session + + async def _set_cookie(self) -> None: + cookie = Morsel() + + cookie.set("arl", self._arl_token, self._arl_token) + cookie.domain = ".deezer.com" + cookie.path = "/" + cookie.httponly = {"HttpOnly": True} + + self.session.cookie_jar.update_cookies(BaseCookie({"arl": cookie}), URL(GW_LIGHT_URL)) + + async def _update_user_data(self) -> None: + user_data = await self._gw_api_call("deezer.getUserData", False) + if not user_data["results"]["USER"]["USER_ID"]: + await self._set_cookie() + user_data = await self._gw_api_call("deezer.getUserData", False) + + if not user_data["results"]["OFFER_ID"]: + msg = "Free subscriptions cannot be used in MA. Make sure you set a valid ARL." + raise DeezerGWError(msg) + + self._gw_csrf_token = user_data["results"]["checkForm"] + self._license = user_data["results"]["USER"]["OPTIONS"]["license_token"] + self._license_expiration_timestamp = user_data["results"]["USER"]["OPTIONS"][ + "expiration_timestamp" + ] + web_qualities = user_data["results"]["USER"]["OPTIONS"]["web_sound_quality"] + mobile_qualities = user_data["results"]["USER"]["OPTIONS"]["mobile_sound_quality"] + if web_qualities["high"] or mobile_qualities["high"]: + self.formats.insert(0, {"cipher": "BF_CBC_STRIPE", "format": "MP3_320"}) + if web_qualities["lossless"] or mobile_qualities["lossless"]: + self.formats.insert(0, {"cipher": "BF_CBC_STRIPE", "format": "FLAC"}) + + self.user_country = user_data["results"]["COUNTRY"] + + async def setup(self) -> None: + """Call this to let the client get its cookies, license and tokens.""" + await self._set_cookie() + await self._update_user_data() + + async def _get_license(self): + if ( + self._license_expiration_timestamp + < (datetime.datetime.now() + datetime.timedelta(days=1)).timestamp() + ): + await self._update_user_data() + return self._license + + async def _gw_api_call( + self, method, use_csrf_token=True, args=None, params=None, http_method="POST", retry=True + ): + csrf_token = self._gw_csrf_token if use_csrf_token else "null" + if params is None: + params = {} + parameters = {"api_version": "1.0", "api_token": csrf_token, "input": "3", "method": method} + parameters |= params + result = await self.session.request( + http_method, + GW_LIGHT_URL, + params=parameters, + timeout=30, + json=args, + headers={"User-Agent": USER_AGENT_HEADER}, + ) + result_json = await result.json() + + if result_json["error"]: + if retry: + await self._update_user_data() + return await self._gw_api_call( + method, use_csrf_token, args, params, http_method, False + ) + else: + msg = "Failed to call GW-API" + raise DeezerGWError(msg, result_json["error"]) + return result_json + + async def get_song_data(self, track_id): + """Get data such as the track token for a given track.""" + return await self._gw_api_call("song.getData", args={"SNG_ID": track_id}) + + async def get_deezer_track_urls(self, track_id): + """Get the URL for a given track id.""" + dz_license = await self._get_license() + song_data = await self.get_song_data(track_id) + track_token = song_data["results"]["TRACK_TOKEN"] + url_data = { + "license_token": dz_license, + "media": [ + { + "type": "FULL", + "formats": self.formats, + } + ], + "track_tokens": [track_token], + } + url_response = await self.session.post( + "https://media.deezer.com/v1/get_url", + json=url_data, + headers={"User-Agent": USER_AGENT_HEADER}, + ) + result_json = await url_response.json() + + if error := result_json["data"][0].get("errors"): + msg = "Received an error from API" + raise DeezerGWError(msg, error) + + return result_json["data"][0]["media"][0], song_data["results"] + + async def log_listen( + self, next_track: str | None = None, last_track: StreamDetails | None = None + ) -> None: + """Log the next and/or previous track of the current playback queue.""" + if not (next_track or last_track): + msg = "last or current track information must be provided." + raise DeezerGWError(msg) + + payload = {} + + if next_track: + payload["next_media"] = {"media": {"id": next_track, "type": "song"}} + + if last_track: + seconds_streamed = min( + utc_timestamp() - last_track.data["start_ts"], + last_track.seconds_streamed, + ) + + payload["params"] = { + "media": { + "id": last_track.item_id, + "type": "song", + "format": last_track.data["format"], + }, + "type": 1, + "stat": { + "seek": 1 if seconds_streamed < last_track.duration else 0, + "pause": 0, + "sync": 0, + "next": bool(next_track), + }, + "lt": int(seconds_streamed), + "ctxt": {"t": "search_page", "id": last_track.item_id}, + "dev": {"v": "10020230525142740", "t": 0}, + "ls": [], + "ts_listen": int(last_track.data["start_ts"]), + "is_shuffle": False, + "stream_id": str(last_track.data["stream_id"]), + } + + await self._gw_api_call("log.listen", args=payload) diff --git a/music_assistant/providers/deezer/icon.svg b/music_assistant/providers/deezer/icon.svg new file mode 100644 index 00000000..1c6170d3 --- /dev/null +++ b/music_assistant/providers/deezer/icon.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" > +<path fill-rule="evenodd" clip-rule="evenodd" d="M41.0955 7.32313C41.5396 4.74914 42.1912 3.13054 42.913 3.12744H42.9146C44.2606 3.13208 45.3517 8.7454 45.3517 15.6759C45.3517 22.6063 44.259 28.2243 42.9115 28.2243C42.3591 28.2243 41.8494 27.2704 41.4389 25.6719C40.7903 31.5233 39.4443 35.5459 37.8862 35.5459C36.6806 35.5459 35.5986 33.1296 34.8722 29.3188C34.3762 36.5662 33.1279 41.708 31.6689 41.708C30.7533 41.708 29.9185 39.6705 29.3005 36.3529C28.5573 43.2014 26.8405 48 24.8382 48C22.836 48 21.1162 43.2029 20.376 36.3529C19.7625 39.6705 18.9278 41.708 18.0075 41.708C16.5486 41.708 15.3033 36.5662 14.8043 29.3188C14.0779 33.1296 12.999 35.5459 11.7903 35.5459C10.2337 35.5459 8.88621 31.5249 8.23763 25.6719C7.83017 27.2751 7.31741 28.2243 6.76497 28.2243C5.41745 28.2243 4.32478 22.6063 4.32478 15.6759C4.32478 8.7454 5.41745 3.12744 6.76497 3.12744C7.48833 3.12744 8.13538 4.75068 8.58405 7.32313C9.30283 2.88473 10.4703 0 11.7903 0C13.3576 0 14.7158 4.07975 15.3583 10.0038C15.987 5.69216 16.9408 2.94348 18.0091 2.94348C19.5061 2.94348 20.7789 8.34964 21.2505 15.8908C22.1371 12.0243 23.4205 9.59876 24.8413 9.59876C26.2621 9.59876 27.5455 12.0259 28.4306 15.8908C28.9037 8.34964 30.1749 2.94348 31.672 2.94348C32.7387 2.94348 33.691 5.69216 34.3228 10.0038C34.9637 4.07975 36.3219 0 37.8892 0C39.2047 0 40.3767 2.88628 41.0955 7.32313ZM0.837891 14.4417C0.837891 11.3436 1.45748 8.83142 2.22204 8.83142C2.9866 8.83142 3.60619 11.3436 3.60619 14.4417C3.60619 17.5397 2.9866 20.0519 2.22204 20.0519C1.45748 20.0519 0.837891 17.5397 0.837891 14.4417ZM46.0693 14.4417C46.0693 11.3436 46.6888 8.83142 47.4534 8.83142C48.218 8.83142 48.8376 11.3436 48.8376 14.4417C48.8376 17.5397 48.218 20.0519 47.4534 20.0519C46.6888 20.0519 46.0693 17.5397 46.0693 14.4417Z" fill="#A238FF"/> +</svg> diff --git a/music_assistant/providers/deezer/manifest.json b/music_assistant/providers/deezer/manifest.json new file mode 100644 index 00000000..0324e93e --- /dev/null +++ b/music_assistant/providers/deezer/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "deezer", + "name": "Deezer", + "description": "Support for the Deezer streaming provider in Music Assistant.", + "codeowners": ["@arctixdev", "@micha91"], + "documentation": "https://music-assistant.io/music-providers/deezer/", + "requirements": ["deezer-python-async==0.3.0", "pycryptodome==3.21.0"], + "multi_instance": true +} diff --git a/music_assistant/providers/dlna/__init__.py b/music_assistant/providers/dlna/__init__.py new file mode 100644 index 00000000..97857d01 --- /dev/null +++ b/music_assistant/providers/dlna/__init__.py @@ -0,0 +1,612 @@ +"""DLNA/uPNP Player provider for Music Assistant. + +Most of this code is based on the implementation within Home Assistant: +https://github.com/home-assistant/core/blob/dev/homeassistant/components/dlna_dmr + +All rights/credits reserved. +""" + +from __future__ import annotations + +import asyncio +import functools +import logging +import time +from contextlib import suppress +from dataclasses import dataclass, field +from ipaddress import IPv4Address +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar + +from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client_factory import UpnpFactory +from async_upnp_client.exceptions import UpnpError, UpnpResponseError +from async_upnp_client.profiles.dlna import DmrDevice, TransportState +from async_upnp_client.search import async_search +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_ENABLE_ICY_METADATA, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, + CONF_ENTRY_HTTP_PROFILE, + ConfigEntry, + ConfigValueType, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.constants import CONF_ENFORCE_MP3, CONF_PLAYERS, VERBOSE_LOG_LEVEL +from music_assistant.helpers import DLNANotifyServer +from music_assistant.helpers.didl_lite import create_didl_metadata +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable, Coroutine, Sequence + + from async_upnp_client.client import UpnpRequester, UpnpService, UpnpStateVariable + from async_upnp_client.utils import CaseInsensitiveDict + from music_assistant_models.config_entries import PlayerConfig, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +PLAYER_CONFIG_ENTRIES = ( + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_HTTP_PROFILE, + CONF_ENTRY_ENABLE_ICY_METADATA, + # enable flow mode by default because + # most dlna players do not support enqueueing + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, + create_sample_rates_config_entry(192000, 24, 96000, 24), +) + + +CONF_NETWORK_SCAN = "network_scan" + +_DLNAPlayerProviderT = TypeVar("_DLNAPlayerProviderT", bound="DLNAPlayerProvider") +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return DLNAPlayerProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_NETWORK_SCAN, + type=ConfigEntryType.BOOLEAN, + label="Allow network scan for discovery", + default_value=False, + description="Enable network scan for discovery of players. \n" + "Can be used if (some of) your players are not automatically discovered.", + ), + ) + + +def catch_request_errors( + func: Callable[Concatenate[_DLNAPlayerProviderT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_DLNAPlayerProviderT, _P], Coroutine[Any, Any, _R | None]]: + """Catch UpnpError errors.""" + + @functools.wraps(func) + async def wrapper(self: _DLNAPlayerProviderT, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: + """Catch UpnpError errors and check availability before and after request.""" + player_id = kwargs["player_id"] if "player_id" in kwargs else args[0] + dlna_player = self.dlnaplayers[player_id] + dlna_player.last_command = time.time() + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + self.logger.debug( + "Handling command %s for player %s", + func.__name__, + dlna_player.player.display_name, + ) + if not dlna_player.available: + self.logger.warning("Device disappeared when trying to call %s", func.__name__) + return None + try: + return await func(self, *args, **kwargs) + except UpnpError as err: + dlna_player.force_poll = True + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + self.logger.exception("Error during call %s: %r", func.__name__, err) + else: + self.logger.error("Error during call %s: %r", func.__name__, str(err)) + return None + + return wrapper + + +@dataclass +class DLNAPlayer: + """Class that holds all dlna variables for a player.""" + + udn: str # = player_id + player: Player # mass player + description_url: str # last known location (description.xml) url + + device: DmrDevice | None = None + lock: asyncio.Lock = field( + default_factory=asyncio.Lock + ) # Held when connecting or disconnecting the device + force_poll: bool = False + ssdp_connect_failed: bool = False + + # Track BOOTID in SSDP advertisements for device changes + bootid: int | None = None + last_seen: float = field(default_factory=time.time) + last_command: float = field(default_factory=time.time) + + def update_attributes(self) -> None: + """Update attributes of the MA Player from DLNA state.""" + # generic attributes + + if self.available: + self.player.available = True + self.player.name = self.device.name + self.player.volume_level = int((self.device.volume_level or 0) * 100) + self.player.volume_muted = self.device.is_volume_muted or False + self.player.state = self.get_state(self.device) + self.player.current_item_id = self.device.current_track_uri or "" + if self.player.player_id in self.player.current_item_id: + self.player.active_source = self.player.player_id + elif "spotify" in self.player.current_item_id: + self.player.active_source = "spotify" + elif self.player.current_item_id.startswith("http"): + self.player.active_source = "http" + else: + # TODO: handle other possible sources here + self.player.active_source = None + if self.device.media_position: + # only update elapsed_time if the device actually reports it + self.player.elapsed_time = float(self.device.media_position) + if self.device.media_position_updated_at is not None: + self.player.elapsed_time_last_updated = ( + self.device.media_position_updated_at.timestamp() + ) + else: + # device is unavailable + self.player.available = False + + @property + def available(self) -> bool: + """Device is available when we have a connection to it.""" + return self.device is not None and self.device.profile_device.available + + @staticmethod + def get_state(device: DmrDevice) -> PlayerState: + """Return current PlayerState of the player.""" + if device.transport_state is None: + return PlayerState.IDLE + if device.transport_state in ( + TransportState.PLAYING, + TransportState.TRANSITIONING, + ): + return PlayerState.PLAYING + if device.transport_state in ( + TransportState.PAUSED_PLAYBACK, + TransportState.PAUSED_RECORDING, + ): + return PlayerState.PAUSED + if device.transport_state == TransportState.VENDOR_DEFINED: + # Unable to map this state to anything reasonable, fallback to idle + return PlayerState.IDLE + + return PlayerState.IDLE + + +class DLNAPlayerProvider(PlayerProvider): + """DLNA Player provider.""" + + dlnaplayers: dict[str, DLNAPlayer] | None = None + _discovery_running: bool = False + + lock: asyncio.Lock + requester: UpnpRequester + upnp_factory: UpnpFactory + notify_server: DLNANotifyServer + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.dlnaplayers = {} + self.lock = asyncio.Lock() + # silence the async_upnp_client logger + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("async_upnp_client").setLevel(logging.DEBUG) + else: + logging.getLogger("async_upnp_client").setLevel(self.logger.level + 10) + self.requester = AiohttpSessionRequester(self.mass.http_session, with_sleep=True) + self.upnp_factory = UpnpFactory(self.requester, non_strict=True) + self.notify_server = DLNANotifyServer(self.requester, self.mass) + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + self.mass.streams.unregister_dynamic_route("/notify", "NOTIFY") + async with TaskManager(self.mass) as tg: + for dlna_player in self.dlnaplayers.values(): + tg.create_task(self._device_disconnect(dlna_player)) + + async def get_player_config_entries( + self, + player_id: str, + ) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + base_entries = await super().get_player_config_entries(player_id) + return base_entries + PLAYER_CONFIG_ENTRIES + + async def on_player_config_change( + self, + config: PlayerConfig, + changed_keys: set[str], + ) -> None: + """Call (by config manager) when the configuration of a player changes.""" + if dlna_player := self.dlnaplayers.get(config.player_id): + # reset player features based on config values + self._set_player_features(dlna_player) + else: + # run discovery to catch any re-enabled players + self.mass.create_task(self.discover_players()) + + @catch_request_errors + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + dlna_player = self.dlnaplayers[player_id] + assert dlna_player.device is not None + await dlna_player.device.async_stop() + + @catch_request_errors + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + dlna_player = self.dlnaplayers[player_id] + assert dlna_player.device is not None + await dlna_player.device.async_play() + + @catch_request_errors + async def play_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle PLAY MEDIA on given player.""" + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): + media.uri = media.uri.replace(".flac", ".mp3") + dlna_player = self.dlnaplayers[player_id] + # always clear queue (by sending stop) first + if dlna_player.device.can_stop: + await self.cmd_stop(player_id) + didl_metadata = create_didl_metadata(media) + title = media.title or media.uri + await dlna_player.device.async_set_transport_uri(media.uri, title, didl_metadata) + # Play it + await dlna_player.device.async_wait_for_can_play(10) + # optimistically set this timestamp to help in case of a player + # that does not report the progress + now = time.time() + dlna_player.player.elapsed_time = 0 + dlna_player.player.elapsed_time_last_updated = now + await dlna_player.device.async_play() + # force poll the device + for sleep in (1, 2): + await asyncio.sleep(sleep) + dlna_player.force_poll = True + await self.poll_player(dlna_player.udn) + + @catch_request_errors + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next queue item on the player.""" + dlna_player = self.dlnaplayers[player_id] + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): + media.uri = media.uri.replace(".flac", ".mp3") + didl_metadata = create_didl_metadata(media) + title = media.title or media.uri + try: + await dlna_player.device.async_set_next_transport_uri(media.uri, title, didl_metadata) + except UpnpError: + self.logger.error( + "Enqueuing the next track failed for player %s - " + "the player probably doesn't support this. " + "Enable 'flow mode' for this player.", + dlna_player.player.display_name, + ) + else: + self.logger.debug( + "Enqued next track (%s) to player %s", + title, + dlna_player.player.display_name, + ) + + @catch_request_errors + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + dlna_player = self.dlnaplayers[player_id] + assert dlna_player.device is not None + if dlna_player.device.can_pause: + await dlna_player.device.async_pause() + else: + await dlna_player.device.async_stop() + + @catch_request_errors + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + dlna_player = self.dlnaplayers[player_id] + assert dlna_player.device is not None + await dlna_player.device.async_set_volume_level(volume_level / 100) + + @catch_request_errors + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + dlna_player = self.dlnaplayers[player_id] + assert dlna_player.device is not None + await dlna_player.device.async_mute_volume(muted) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + dlna_player = self.dlnaplayers[player_id] + + # try to reconnect the device if the connection was lost + if not dlna_player.device: + if not dlna_player.force_poll: + return + try: + await self._device_connect(dlna_player) + except UpnpError as err: + raise PlayerUnavailableError from err + + assert dlna_player.device is not None + + try: + now = time.time() + do_ping = dlna_player.force_poll or (now - dlna_player.last_seen) > 60 + with suppress(ValueError): + await dlna_player.device.async_update(do_ping=do_ping) + dlna_player.last_seen = now if do_ping else dlna_player.last_seen + except UpnpError as err: + self.logger.debug("Device unavailable: %r", err) + await self._device_disconnect(dlna_player) + raise PlayerUnavailableError from err + finally: + dlna_player.force_poll = False + + async def discover_players(self, use_multicast: bool = False) -> None: + """Discover DLNA players on the network.""" + if self._discovery_running: + return + try: + self._discovery_running = True + self.logger.debug("DLNA discovery started...") + allow_network_scan = self.config.get_value(CONF_NETWORK_SCAN) + discovered_devices: set[str] = set() + + async def on_response(discovery_info: CaseInsensitiveDict) -> None: + """Process discovered device from ssdp search.""" + ssdp_st: str = discovery_info.get("st", discovery_info.get("nt")) + if not ssdp_st: + return + + if "MediaRenderer" not in ssdp_st: + # we're only interested in MediaRenderer devices + return + + ssdp_usn: str = discovery_info["usn"] + ssdp_udn: str | None = discovery_info.get("_udn") + if not ssdp_udn and ssdp_usn.startswith("uuid:"): + ssdp_udn = ssdp_usn.split("::")[0] + + if ssdp_udn in discovered_devices: + # already processed this device + return + if "rincon" in ssdp_udn.lower(): + # ignore Sonos devices + return + + discovered_devices.add(ssdp_udn) + + await self._device_discovered(ssdp_udn, discovery_info["location"]) + + # we iterate between using a regular and multicast search (if enabled) + if allow_network_scan and use_multicast: + await async_search(on_response, target=(str(IPv4Address("255.255.255.255")), 1900)) + else: + await async_search(on_response) + + finally: + self._discovery_running = False + + def reschedule() -> None: + self.mass.create_task(self.discover_players(use_multicast=not use_multicast)) + + # reschedule self once finished + self.mass.loop.call_later(300, reschedule) + + async def _device_disconnect(self, dlna_player: DLNAPlayer) -> None: + """ + Destroy connections to the device now that it's not available. + + Also call when removing this entity from MA to clean up connections. + """ + async with dlna_player.lock: + if not dlna_player.device: + self.logger.debug("Disconnecting from device that's not connected") + return + + self.logger.debug("Disconnecting from %s", dlna_player.device.name) + + dlna_player.device.on_event = None + old_device = dlna_player.device + dlna_player.device = None + await old_device.async_unsubscribe_services() + + async def _device_discovered(self, udn: str, description_url: str) -> None: + """Handle discovered DLNA player.""" + async with self.lock: + if dlna_player := self.dlnaplayers.get(udn): + # existing player + if dlna_player.description_url == description_url and dlna_player.player.available: + # nothing to do, device is already connected + return + # update description url to newly discovered one + dlna_player.description_url = description_url + else: + # new player detected, setup our DLNAPlayer wrapper + conf_key = f"{CONF_PLAYERS}/{udn}/enabled" + enabled = self.mass.config.get(conf_key, True) + # ignore disabled players + if not enabled: + self.logger.debug("Ignoring disabled player: %s", udn) + return + + dlna_player = DLNAPlayer( + udn=udn, + player=Player( + player_id=udn, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=udn, + available=False, + powered=False, + # device info will be discovered later after connect + device_info=DeviceInfo( + model="unknown", + address=description_url, + manufacturer="unknown", + ), + needs_poll=True, + poll_interval=30, + ), + description_url=description_url, + ) + self.dlnaplayers[udn] = dlna_player + + await self._device_connect(dlna_player) + + self._set_player_features(dlna_player) + dlna_player.update_attributes() + await self.mass.players.register_or_update(dlna_player.player) + + async def _device_connect(self, dlna_player: DLNAPlayer) -> None: + """Connect DLNA/DMR Device.""" + self.logger.debug("Connecting to device at %s", dlna_player.description_url) + + async with dlna_player.lock: + if dlna_player.device: + self.logger.debug("Trying to connect when device already connected") + return + + # Connect to the base UPNP device + upnp_device = await self.upnp_factory.async_create_device(dlna_player.description_url) + + # Create profile wrapper + dlna_player.device = DmrDevice(upnp_device, self.notify_server.event_handler) + + # Subscribe to event notifications + try: + dlna_player.device.on_event = self._handle_event + await dlna_player.device.async_subscribe_services(auto_resubscribe=True) + except UpnpResponseError as err: + # Device rejected subscription request. This is OK, variables + # will be polled instead. + self.logger.debug("Device rejected subscription: %r", err) + except UpnpError as err: + # Don't leave the device half-constructed + dlna_player.device.on_event = None + dlna_player.device = None + self.logger.debug("Error while subscribing during device connect: %r", err) + raise + else: + # connect was successful, update device info + dlna_player.player.device_info = DeviceInfo( + model=dlna_player.device.model_name, + address=dlna_player.device.device.presentation_url + or dlna_player.description_url, + manufacturer=dlna_player.device.manufacturer, + ) + + def _handle_event( + self, + service: UpnpService, + state_variables: Sequence[UpnpStateVariable], + ) -> None: + """Handle state variable(s) changed event from DLNA device.""" + udn = service.device.udn + dlna_player = self.dlnaplayers[udn] + + if not state_variables: + # Indicates a failure to resubscribe, check if device is still available + dlna_player.force_poll = True + return + + if service.service_id == "urn:upnp-org:serviceId:AVTransport": + for state_variable in state_variables: + # Force a state refresh when player begins or pauses playback + # to update the position info. + if state_variable.name == "TransportState" and state_variable.value in ( + TransportState.PLAYING, + TransportState.PAUSED_PLAYBACK, + ): + dlna_player.force_poll = True + self.mass.create_task(self.poll_player(dlna_player.udn)) + self.logger.debug( + "Received new state from event for Player %s: %s", + dlna_player.player.display_name, + state_variable.value, + ) + + dlna_player.last_seen = time.time() + self.mass.create_task(self._update_player(dlna_player)) + + async def _update_player(self, dlna_player: DLNAPlayer) -> None: + """Update DLNA Player.""" + prev_url = dlna_player.player.current_item_id + prev_state = dlna_player.player.state + dlna_player.update_attributes() + current_url = dlna_player.player.current_item_id + current_state = dlna_player.player.state + + if (prev_url != current_url) or (prev_state != current_state): + # fetch track details on state or url change + dlna_player.force_poll = True + + # let the MA player manager work out if something actually updated + self.mass.players.update(dlna_player.udn) + + def _set_player_features(self, dlna_player: DLNAPlayer) -> None: + """Set Player Features based on config values and capabilities.""" + supported_features: set[PlayerFeature] = set() + if not self.mass.config.get_raw_player_config_value( + dlna_player.udn, + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED.key, + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED.default_value, + ): + supported_features.add(PlayerFeature.ENQUEUE) + + if dlna_player.device.has_volume_level: + supported_features.add(PlayerFeature.VOLUME_SET) + if dlna_player.device.has_volume_mute: + supported_features.add(PlayerFeature.VOLUME_MUTE) + if dlna_player.device.has_pause: + supported_features.add(PlayerFeature.PAUSE) + dlna_player.player.supported_features = tuple(supported_features) diff --git a/music_assistant/providers/dlna/helpers.py b/music_assistant/providers/dlna/helpers.py new file mode 100644 index 00000000..32242ae8 --- /dev/null +++ b/music_assistant/providers/dlna/helpers.py @@ -0,0 +1,50 @@ +"""Various helpers and utils for the DLNA Player Provider.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aiohttp.web import Request, Response +from async_upnp_client.const import HttpRequest +from async_upnp_client.event_handler import UpnpEventHandler, UpnpNotifyServer + +if TYPE_CHECKING: + from async_upnp_client.client import UpnpRequester + + from music_assistant import MusicAssistant + + +class DLNANotifyServer(UpnpNotifyServer): + """Notify server for async_upnp_client which uses the MA webserver.""" + + def __init__( + self, + requester: UpnpRequester, + mass: MusicAssistant, + ) -> None: + """Initialize.""" + self.mass = mass + self.event_handler = UpnpEventHandler(self, requester) + self.mass.streams.register_dynamic_route("/notify", self._handle_request, method="NOTIFY") + + async def _handle_request(self, request: Request) -> Response: + """Handle incoming requests.""" + if request.method != "NOTIFY": + return Response(status=405) + + # transform aiohttp request to async_upnp_client request + http_request = HttpRequest( + method=request.method, + url=request.url, + headers=request.headers, + body=await request.text(), + ) + + status = await self.event_handler.handle_notify(http_request) + + return Response(status=status) + + @property + def callback_url(self) -> str: + """Return callback URL on which we are callable.""" + return f"{self.mass.streams.base_url}/notify" diff --git a/music_assistant/providers/dlna/icon.svg b/music_assistant/providers/dlna/icon.svg new file mode 100644 index 00000000..10e19efa --- /dev/null +++ b/music_assistant/providers/dlna/icon.svg @@ -0,0 +1,3 @@ +<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.1665 21.9022C7.84476 21.5378 5.56352 20.4202 4.00848 18.8854C3.47569 18.3595 3.43927 18.3076 3.45642 18.0976C3.48828 17.7075 3.43013 17.7138 6.98871 17.714C10.5313 17.714 10.6981 17.7287 11.4291 18.1005C11.7345 18.2559 11.9955 18.4747 12.5115 19.0082C13.452 19.9804 13.9907 20.227 15.0225 20.1576C15.6386 20.1162 16.114 19.9338 16.5999 19.5526C18.1253 18.3556 18.1253 15.9318 16.5999 14.7348C16.0779 14.3252 15.6423 14.167 14.9332 14.1297C13.9321 14.0769 13.3736 14.3466 12.458 15.3249C12.0491 15.7618 11.7985 15.9708 11.4804 16.1401C10.7024 16.5543 10.8273 16.5438 6.6526 16.5438H2.86366L2.52755 16.3796C2.10792 16.1745 1.89097 15.9502 1.66513 15.488C1.21208 14.5606 1.00037 13.4768 1 12.0828C0.999609 10.6391 1.28851 9.17976 1.73179 8.3862C1.93859 8.01598 2.33458 7.70094 2.75902 7.56895C3.18116 7.43769 10.1586 7.44557 10.7019 7.57793C11.3888 7.74527 11.7868 8.00512 12.4888 8.74454C12.9931 9.27582 13.2167 9.46278 13.5334 9.6183C14.7074 10.1948 15.9648 9.96823 16.884 9.01457C17.5452 8.32868 17.7848 7.6561 17.729 6.64356C17.7016 6.14806 17.6688 6.02184 17.4468 5.55889C16.7715 4.15088 15.2237 3.49487 13.8332 4.02734C13.3099 4.22773 13.0553 4.42066 12.3971 5.11545C11.7578 5.79025 11.3333 6.06776 10.7148 6.215C10.5454 6.25535 9.16383 6.28065 7.0787 6.28161C3.34562 6.28332 3.44458 6.29413 3.44388 5.88493C3.44359 5.71455 3.52244 5.60501 3.95261 5.17816C5.16372 3.9764 6.87268 2.97953 8.60415 2.46482C9.64901 2.15422 10.4215 2.04102 11.7249 2.00753C13.1133 1.97185 13.9277 2.05834 15.1194 2.36803C18.5393 3.25682 21.2933 5.60754 22.4801 8.651C22.7276 9.28574 23 10.3016 23 10.59C23 10.9149 22.8201 11.2127 22.5463 11.3412C22.3107 11.4518 22.0379 11.4581 17.4999 11.4584C12.2197 11.4587 12.2173 11.4586 11.4499 11.0724C11.1458 10.9193 10.9018 10.7139 10.409 10.196C9.73955 9.49239 9.38402 9.24803 8.79151 9.08425C8.38185 8.97101 7.70716 8.97632 7.26128 9.09629C5.87257 9.46992 4.90505 11.0192 5.14385 12.4869C5.43295 14.2637 7.12291 15.4117 8.78937 14.9632C9.38392 14.8032 9.72805 14.5666 10.411 13.8485C10.9194 13.314 11.1363 13.1334 11.4553 12.9792C12.2411 12.5992 12.2329 12.5997 17.5305 12.5986C22.9666 12.5975 22.6695 12.5716 22.9035 13.067C23.009 13.2903 23.0138 13.359 22.9501 13.7428C22.7712 14.8206 22.1317 16.3376 21.4342 17.3384C19.7464 19.7605 16.9625 21.446 13.8865 21.9081C13.0536 22.0332 10.9805 22.0299 10.1665 21.9022Z" fill="#4AAA42"/> +</svg> diff --git a/music_assistant/providers/dlna/manifest.json b/music_assistant/providers/dlna/manifest.json new file mode 100644 index 00000000..9c7e4817 --- /dev/null +++ b/music_assistant/providers/dlna/manifest.json @@ -0,0 +1,16 @@ +{ + "type": "player", + "domain": "dlna", + "name": "UPnP/DLNA Player provider", + "description": "Support for players that are compatible with the UPnP/DLNA (DMR) standard.", + "codeowners": [ + "@music-assistant" + ], + "requirements": [ + "async-upnp-client==0.41.0" + ], + "documentation": "https://music-assistant.io/player-support/dlna/", + "multi_instance": false, + "builtin": false, + "icon": "dlna" +} diff --git a/music_assistant/providers/fanarttv/__init__.py b/music_assistant/providers/fanarttv/__init__.py new file mode 100644 index 00000000..eaaa533b --- /dev/null +++ b/music_assistant/providers/fanarttv/__init__.py @@ -0,0 +1,194 @@ +"""Fanart.tv Metadata provider for Music Assistant.""" + +from __future__ import annotations + +from json import JSONDecodeError +from typing import TYPE_CHECKING + +import aiohttp.client_exceptions +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature +from music_assistant_models.media_items import ImageType, MediaItemImage, MediaItemMetadata + +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.metadata_provider import MetadataProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.media_items import Album, Artist + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +SUPPORTED_FEATURES = ( + ProviderFeature.ARTIST_METADATA, + ProviderFeature.ALBUM_METADATA, +) + +CONF_ENABLE_ARTIST_IMAGES = "enable_artist_images" +CONF_ENABLE_ALBUM_IMAGES = "enable_album_images" +CONF_CLIENT_KEY = "client_key" + +IMG_MAPPING = { + "artistthumb": ImageType.THUMB, + "hdmusiclogo": ImageType.LOGO, + "musicbanner": ImageType.BANNER, + "artistbackground": ImageType.FANART, +} + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return FanartTvMetadataProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_ENABLE_ARTIST_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist images.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_ALBUM_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of album image(s).", + default_value=True, + ), + ConfigEntry( + key=CONF_CLIENT_KEY, + type=ConfigEntryType.SECURE_STRING, + label="VIP Member Personal API Key (optional)", + description="Support this metadata provider by becoming a VIP Member, " + "resulting in higher rate limits and faster response times among other benefits. " + "See https://wiki.fanart.tv/General/personal%20api/ for more information.", + required=False, + ), + ) + + +class FanartTvMetadataProvider(MetadataProvider): + """Fanart.tv Metadata provider.""" + + throttler: Throttler + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.cache = self.mass.cache + if self.config.get_value(CONF_CLIENT_KEY): + # loosen the throttler when a personal client key is used + self.throttler = Throttler(rate_limit=1, period=1) + else: + self.throttler = Throttler(rate_limit=1, period=30) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: + """Retrieve metadata for artist on fanart.tv.""" + if not artist.mbid: + return None + if not self.config.get_value(CONF_ENABLE_ARTIST_IMAGES): + return None + self.logger.debug("Fetching metadata for Artist %s on Fanart.tv", artist.name) + if data := await self._get_data(f"music/{artist.mbid}"): + metadata = MediaItemMetadata() + metadata.images = [] + for key, img_type in IMG_MAPPING.items(): + items = data.get(key) + if not items: + continue + for item in items: + metadata.images.append( + MediaItemImage( + type=img_type, + path=item["url"], + provider=self.domain, + remotely_accessible=True, + ) + ) + return metadata + return None + + async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: + """Retrieve metadata for album on fanart.tv.""" + if (mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP)) is None: + return None + if not self.config.get_value(CONF_ENABLE_ALBUM_IMAGES): + return None + self.logger.debug("Fetching metadata for Album %s on Fanart.tv", album.name) + if data := await self._get_data(f"music/albums/{mbid}"): + if data and data.get("albums"): + data = data["albums"][mbid] + metadata = MediaItemMetadata() + metadata.images = [] + for key, img_type in IMG_MAPPING.items(): + items = data.get(key) + if not items: + continue + for item in items: + metadata.images.append( + MediaItemImage( + type=img_type, + path=item["url"], + provider=self.domain, + remotely_accessible=True, + ) + ) + return metadata + return None + + @use_cache(86400 * 30) + async def _get_data(self, endpoint, **kwargs) -> dict | None: + """Get data from api.""" + url = f"http://webservice.fanart.tv/v3/{endpoint}" + headers = { + "api-key": app_var(4), + } + if client_key := self.config.get_value(CONF_CLIENT_KEY): + headers["client_key"] = client_key + async with ( + self.throttler, + self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, + ): + try: + result = await response.json() + except ( + aiohttp.client_exceptions.ContentTypeError, + JSONDecodeError, + ): + self.logger.error("Failed to retrieve %s", endpoint) + text_result = await response.text() + self.logger.debug(text_result) + return None + except ( + aiohttp.client_exceptions.ClientConnectorError, + aiohttp.client_exceptions.ServerDisconnectedError, + ): + self.logger.warning("Failed to retrieve %s", endpoint) + return None + if "error" in result and "limit" in result["error"]: + self.logger.warning(result["error"]) + return None + return result diff --git a/music_assistant/providers/fanarttv/manifest.json b/music_assistant/providers/fanarttv/manifest.json new file mode 100644 index 00000000..a39b2593 --- /dev/null +++ b/music_assistant/providers/fanarttv/manifest.json @@ -0,0 +1,12 @@ +{ + "type": "metadata", + "domain": "fanarttv", + "name": "fanart.tv", + "description": "fanart.tv is a community database of artwork for movies, tv series and music.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "", + "multi_instance": false, + "builtin": true, + "icon": "folder-information" +} diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py new file mode 100644 index 00000000..aa7f1927 --- /dev/null +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -0,0 +1,1121 @@ +"""Filesystem musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import os +import os.path +from typing import TYPE_CHECKING, cast + +import aiofiles +import shortuuid +import xmltodict +from aiofiles.os import wrap +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError, SetupFailedError +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + BrowseFolder, + ContentType, + ImageType, + ItemMapping, + MediaItemImage, + MediaItemType, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, + UniqueList, + is_track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import ( + CONF_PATH, + DB_TABLE_ALBUM_ARTISTS, + DB_TABLE_ALBUM_TRACKS, + DB_TABLE_ALBUMS, + DB_TABLE_ARTISTS, + DB_TABLE_PROVIDER_MAPPINGS, + DB_TABLE_TRACK_ARTISTS, + VARIOUS_ARTISTS_MBID, + VARIOUS_ARTISTS_NAME, +) +from music_assistant.helpers.compare import compare_strings, create_safe_string +from music_assistant.helpers.playlists import parse_m3u, parse_pls +from music_assistant.helpers.tags import AudioTags, parse_tags, split_items +from music_assistant.helpers.util import TaskManager, parse_title_and_version +from music_assistant.models.music_provider import MusicProvider + +from .helpers import ( + FileSystemItem, + get_absolute_path, + get_album_dir, + get_artist_dir, + get_relative_path, + sorted_scandir, +) + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +CONF_MISSING_ALBUM_ARTIST_ACTION = "missing_album_artist_action" + +CONF_ENTRY_MISSING_ALBUM_ARTIST = ConfigEntry( + key=CONF_MISSING_ALBUM_ARTIST_ACTION, + type=ConfigEntryType.STRING, + label="Action when a track is missing the Albumartist ID3 tag", + default_value="various_artists", + help_link="https://music-assistant.io/music-providers/filesystem/#tagging-files", + required=False, + options=( + ConfigValueOption("Use Track artist(s)", "track_artist"), + ConfigValueOption("Use Various Artists", "various_artists"), + ConfigValueOption("Use Folder name (if possible)", "folder_name"), + ), +) + +TRACK_EXTENSIONS = ( + "mp3", + "m4a", + "m4b", + "mp4", + "flac", + "wav", + "ogg", + "aiff", + "wma", + "dsf", + "opus", +) +PLAYLIST_EXTENSIONS = ("m3u", "pls", "m3u8") +SUPPORTED_EXTENSIONS = TRACK_EXTENSIONS + PLAYLIST_EXTENSIONS +IMAGE_EXTENSIONS = ("jpg", "jpeg", "JPG", "JPEG", "png", "PNG", "gif", "GIF") +SEEKABLE_FILES = (ContentType.MP3, ContentType.WAV, ContentType.FLAC) + + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, +) + +listdir = wrap(os.listdir) +isdir = wrap(os.path.isdir) +isfile = wrap(os.path.isfile) +exists = wrap(os.path.exists) +makedirs = wrap(os.makedirs) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + conf_path = config.get_value(CONF_PATH) + if not await isdir(conf_path): + msg = f"Music Directory {conf_path} does not exist" + raise SetupFailedError(msg) + prov = LocalFileSystemProvider(mass, manifest, config) + prov.base_path = str(config.get_value(CONF_PATH)) + await prov.check_write_access() + return prov + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry(key="path", type=ConfigEntryType.STRING, label="Path", default_value="/media"), + CONF_ENTRY_MISSING_ALBUM_ARTIST, + ) + + +class LocalFileSystemProvider(MusicProvider): + """ + Implementation of a musicprovider for (local) files. + + Reads ID3 tags from file and falls back to parsing filename. + Optionally reads metadata from nfo files and images in folder structure <artist>/<album>. + Supports m3u files for playlists. + """ + + base_path: str + write_access: bool = False + scan_limiter = asyncio.Semaphore(25) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + if self.write_access: + return ( + *SUPPORTED_FEATURES, + ProviderFeature.PLAYLIST_CREATE, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ) + return SUPPORTED_FEATURES + + @property + def is_streaming_provider(self) -> bool: + """Return True if the provider is a streaming provider.""" + return False + + async def search( + self, + search_query: str, + media_types: list[MediaType] | None, + limit: int = 5, + ) -> SearchResults: + """Perform search on this file based musicprovider.""" + result = SearchResults() + # searching the filesystem is slow and unreliable, + # so instead we just query the db... + if media_types is None or MediaType.TRACK in media_types: + result.tracks = await self.mass.music.tracks._get_library_items_by_query( + search=search_query, provider=self.instance_id, limit=limit + ) + + if media_types is None or MediaType.ALBUM in media_types: + result.albums = await self.mass.music.albums._get_library_items_by_query( + search=search_query, + provider=self.instance_id, + limit=limit, + ) + + if media_types is None or MediaType.ARTIST in media_types: + result.artists = await self.mass.music.artists._get_library_items_by_query( + search=search_query, + provider=self.instance_id, + limit=limit, + ) + if media_types is None or MediaType.PLAYLIST in media_types: + result.playlists = await self.mass.music.playlists._get_library_items_by_query( + search=search_query, + provider=self.instance_id, + limit=limit, + ) + return result + + async def browse(self, path: str) -> list[MediaItemType | ItemMapping]: + """Browse this provider's items. + + :param path: The path to browse, (e.g. provid://artists). + """ + items: list[MediaItemType | ItemMapping] = [] + item_path = path.split("://", 1)[1] + if not item_path: + item_path = "" + async for item in self.listdir(item_path, recursive=False, sort=True): + if not item.is_dir and ("." not in item.filename or not item.ext): + # skip system files and files without extension + continue + + if item.is_dir: + items.append( + BrowseFolder( + item_id=item.path, + provider=self.instance_id, + path=f"{self.instance_id}://{item.path}", + name=item.filename, + ) + ) + elif item.ext in TRACK_EXTENSIONS: + items.append( + ItemMapping( + media_type=MediaType.TRACK, + item_id=item.path, + provider=self.instance_id, + name=item.filename, + ) + ) + elif item.ext in PLAYLIST_EXTENSIONS: + items.append( + ItemMapping( + media_type=MediaType.PLAYLIST, + item_id=item.path, + provider=self.instance_id, + name=item.filename, + ) + ) + return items + + async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: + """Run library sync for this provider.""" + assert self.mass.music.database + file_checksums: dict[str, str] = {} + query = ( + f"SELECT provider_item_id, details FROM {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE provider_instance = '{self.instance_id}' " + "AND media_type in ('track', 'playlist')" + ) + for db_row in await self.mass.music.database.get_rows_from_query(query, limit=0): + file_checksums[db_row["provider_item_id"]] = str(db_row["details"]) + # find all music files in the music directory and all subfolders + # we work bottom up, as-in we derive all info from the tracks + cur_filenames = set() + prev_filenames = set(file_checksums.keys()) + async with TaskManager(self.mass, 25) as tm: + async for item in self.listdir("", recursive=True, sort=False): + if "." not in item.filename or not item.ext: + # skip system files and files without extension + continue + + if item.ext not in SUPPORTED_EXTENSIONS: + # unsupported file extension + continue + + cur_filenames.add(item.path) + + # continue if the item did not change (checksum still the same) + prev_checksum = file_checksums.get(item.path) + if item.checksum == prev_checksum: + continue + + await tm.create_task_with_limit(self._process_item(item, prev_checksum)) + + # work out deletions + deleted_files = prev_filenames - cur_filenames + await self._process_deletions(deleted_files) + + # process orphaned albums and artists + await self._process_orphaned_albums_and_artists() + + async def _process_item(self, item: FileSystemItem, prev_checksum: str | None) -> None: + """Process a single item.""" + try: + self.logger.debug("Processing: %s", item.path) + if item.ext in TRACK_EXTENSIONS: + # add/update track to db + # note that filesystem items are always overwriting existing info + # when they are detected as changed + track = await self._parse_track(item) + await self.mass.music.tracks.add_item_to_library( + track, overwrite_existing=prev_checksum is not None + ) + elif item.ext in PLAYLIST_EXTENSIONS: + playlist = await self.get_playlist(item.path) + # add/update] playlist to db + playlist.cache_checksum = item.checksum + # playlist is always favorite + playlist.favorite = True + await self.mass.music.playlists.add_item_to_library( + playlist, + overwrite_existing=prev_checksum is not None, + ) + except Exception as err: + # we don't want the whole sync to crash on one file so we catch all exceptions here + self.logger.error( + "Error processing %s - %s", + item.path, + str(err), + exc_info=err if self.logger.isEnabledFor(logging.DEBUG) else None, + ) + + async def _process_orphaned_albums_and_artists(self) -> None: + """Process deletion of orphaned albums and artists.""" + assert self.mass.music.database + # Remove albums without any tracks + query = ( + f"SELECT item_id FROM {DB_TABLE_ALBUMS} " + f"WHERE item_id not in ( SELECT album_id from {DB_TABLE_ALBUM_TRACKS}) " + f"AND item_id in ( SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE provider_instance = '{self.instance_id}' and media_type = 'album' )" + ) + for db_row in await self.mass.music.database.get_rows_from_query( + query, + limit=100000, + ): + await self.mass.music.albums.remove_item_from_library(db_row["item_id"]) + + # Remove artists without any tracks or albums + query = ( + f"SELECT item_id FROM {DB_TABLE_ARTISTS} " + f"WHERE item_id not in " + f"( select artist_id from {DB_TABLE_TRACK_ARTISTS} " + f"UNION SELECT artist_id from {DB_TABLE_ALBUM_ARTISTS} ) " + f"AND item_id in ( SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE provider_instance = '{self.instance_id}' and media_type = 'artist' )" + ) + for db_row in await self.mass.music.database.get_rows_from_query( + query, + limit=100000, + ): + await self.mass.music.artists.remove_item_from_library(db_row["item_id"]) + + async def _process_deletions(self, deleted_files: set[str]) -> None: + """Process all deletions.""" + # process deleted tracks/playlists + album_ids = set() + artist_ids = set() + for file_path in deleted_files: + _, ext = file_path.rsplit(".", 1) + if ext not in SUPPORTED_EXTENSIONS: + # unsupported file extension + continue + + if ext in PLAYLIST_EXTENSIONS: + controller = self.mass.music.get_controller(MediaType.PLAYLIST) + else: + controller = self.mass.music.get_controller(MediaType.TRACK) + + if library_item := await controller.get_library_item_by_prov_id( + file_path, self.instance_id + ): + if is_track(library_item): + if library_item.album: + album_ids.add(library_item.album.item_id) + # need to fetch the library album to resolve the itemmapping + db_album = await self.mass.music.albums.get_library_item( + library_item.album.item_id + ) + for artist in db_album.artists: + artist_ids.add(artist.item_id) + for artist in library_item.artists: + artist_ids.add(artist.item_id) + await controller.remove_item_from_library(library_item.item_id) + # check if any albums need to be cleaned up + for album_id in album_ids: + if not await self.mass.music.albums.tracks(album_id, "library"): + await self.mass.music.albums.remove_item_from_library(album_id) + # check if any artists need to be cleaned up + for artist_id in artist_ids: + artist_albums = await self.mass.music.artists.albums(artist_id, "library") + artist_tracks = await self.mass.music.artists.tracks(artist_id, "library") + if not (artist_albums or artist_tracks): + await self.mass.music.artists.remove_item_from_library(artist_id) + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + db_artist = await self.mass.music.artists.get_library_item_by_prov_id( + prov_artist_id, self.instance_id + ) + if not db_artist: + # this should not be possible, but just in case + msg = f"Artist not found: {prov_artist_id}" + raise MediaNotFoundError(msg) + # prov_artist_id is either an actual (relative) path or a name (as fallback) + safe_artist_name = create_safe_string(prov_artist_id, lowercase=False, replace_space=False) + if await self.exists(prov_artist_id): + artist_path = prov_artist_id + elif await self.exists(safe_artist_name): + artist_path = safe_artist_name + else: + for prov_mapping in db_artist.provider_mappings: + if prov_mapping.provider_instance != self.instance_id: + continue + if prov_mapping.url: + artist_path = prov_mapping.url + break + else: + # this is an artist without an actual path on disk + # return the info we already have in the db + return db_artist + return await self._parse_artist( + db_artist.name, + sort_name=db_artist.sort_name, + mbid=db_artist.mbid, + artist_path=artist_path, + ) + + async def get_album(self, prov_album_id: str) -> Album: + """Get full album details by id.""" + for track in await self.get_album_tracks(prov_album_id): + for prov_mapping in track.provider_mappings: + if prov_mapping.provider_instance == self.instance_id: + file_item = await self.resolve(prov_mapping.item_id) + full_track = await self._parse_track(file_item) + assert isinstance(full_track.album, Album) + return full_track.album + msg = f"Album not found: {prov_album_id}" + raise MediaNotFoundError(msg) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + # ruff: noqa: PLR0915, PLR0912 + if not await self.exists(prov_track_id): + msg = f"Track path does not exist: {prov_track_id}" + raise MediaNotFoundError(msg) + + file_item = await self.resolve(prov_track_id) + return await self._parse_track(file_item, full_album_metadata=True) + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get full playlist details by id.""" + if not await self.exists(prov_playlist_id): + msg = f"Playlist path does not exist: {prov_playlist_id}" + raise MediaNotFoundError(msg) + + file_item = await self.resolve(prov_playlist_id) + playlist = Playlist( + item_id=file_item.path, + provider=self.instance_id, + name=file_item.name, + provider_mappings={ + ProviderMapping( + item_id=file_item.path, + provider_domain=self.domain, + provider_instance=self.instance_id, + details=file_item.checksum, + ) + }, + ) + playlist.is_editable = ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features + # only playlists in the root are editable - all other are read only + if "/" in prov_playlist_id or "\\" in prov_playlist_id: + playlist.is_editable = False + # we do not (yet) have support to edit/create pls playlists, only m3u files can be edited + if file_item.ext == "pls": + playlist.is_editable = False + playlist.owner = self.name + checksum = str(file_item.checksum) + playlist.cache_checksum = checksum + return playlist + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + # filesystem items are always stored in db so we can query the database + db_album = await self.mass.music.albums.get_library_item_by_prov_id( + prov_album_id, self.instance_id + ) + if db_album is None: + msg = f"Album not found: {prov_album_id}" + raise MediaNotFoundError(msg) + album_tracks = await self.mass.music.albums.get_library_album_tracks(db_album.item_id) + return [ + track + for track in album_tracks + if any(x.provider_instance == self.instance_id for x in track.provider_mappings) + ] + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # paging not (yet) supported + return result + if not await self.exists(prov_playlist_id): + msg = f"Playlist path does not exist: {prov_playlist_id}" + raise MediaNotFoundError(msg) + + _, ext = prov_playlist_id.rsplit(".", 1) + try: + # get playlist file contents + playlist_filename = self.get_absolute_path(prov_playlist_id) + async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: + playlist_data = await _file.read() + if ext in ("m3u", "m3u8"): + playlist_lines = parse_m3u(playlist_data) + else: + playlist_lines = parse_pls(playlist_data) + + for idx, playlist_line in enumerate(playlist_lines, 1): + if track := await self._parse_playlist_line( + playlist_line.path, os.path.dirname(prov_playlist_id) + ): + track.position = idx + result.append(track) + + except Exception as err: + self.logger.warning( + "Error while parsing playlist %s: %s", + prov_playlist_id, + str(err), + exc_info=err if self.logger.isEnabledFor(10) else None, + ) + return result + + async def _parse_playlist_line(self, line: str, playlist_path: str) -> Track | None: + """Try to parse a track from a playlist line.""" + try: + # if a relative path was given in an upper level from the playlist, + # try to resolve it + for parentpart in ("../", "..\\"): + while line.startswith(parentpart): + if len(playlist_path) < 3: + break # guard + playlist_path = parentpart[:-3] + line = line[3:] + + # try to resolve the filename + for filename in (line, os.path.join(playlist_path, line)): + with contextlib.suppress(FileNotFoundError): + item = await self.resolve(filename) + return await self._parse_track(item) + + except MusicAssistantError as err: + self.logger.warning("Could not parse uri/file %s to track: %s", line, str(err)) + + return None + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + if not await self.exists(prov_playlist_id): + msg = f"Playlist path does not exist: {prov_playlist_id}" + raise MediaNotFoundError(msg) + playlist_filename = self.get_absolute_path(prov_playlist_id) + async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: + playlist_data = await _file.read() + for file_path in prov_track_ids: + track = await self.get_track(file_path) + playlist_data += f"\n#EXTINF:{track.duration or 0},{track.name}\n{file_path}\n" + + # write playlist file (always in utf-8) + async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: + await _file.write(playlist_data) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + if not await self.exists(prov_playlist_id): + msg = f"Playlist path does not exist: {prov_playlist_id}" + raise MediaNotFoundError(msg) + _, ext = prov_playlist_id.rsplit(".", 1) + # get playlist file contents + playlist_filename = self.get_absolute_path(prov_playlist_id) + async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: + playlist_data = await _file.read() + # get current contents first + if ext in ("m3u", "m3u8"): + playlist_items = parse_m3u(playlist_data) + else: + playlist_items = parse_pls(playlist_data) + # remove items by index + for i in sorted(positions_to_remove, reverse=True): + # position = index + 1 + del playlist_items[i - 1] + # build new playlist data + new_playlist_data = "#EXTM3U\n" + for item in playlist_items: + new_playlist_data += f"\n#EXTINF:{item.length or 0},{item.title}\n{item.path}\n" + async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: + await _file.write(playlist_data) + + async def create_playlist(self, name: str) -> Playlist: + """Create a new playlist on provider with given name.""" + # creating a new playlist on the filesystem is as easy + # as creating a new (empty) file with the m3u extension... + # filename = await self.resolve(f"{name}.m3u") + filename = f"{name}.m3u" + playlist_filename = self.get_absolute_path(filename) + async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: + await _file.write("#EXTM3U\n") + return await self.get_playlist(filename) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + library_item = await self.mass.music.tracks.get_library_item_by_prov_id( + item_id, self.instance_id + ) + if library_item is None: + # this could be a file that has just been added, try parsing it + file_item = await self.resolve(item_id) + if not (library_item := await self._parse_track(file_item)): + msg = f"Item not found: {item_id}" + raise MediaNotFoundError(msg) + + prov_mapping = next(x for x in library_item.provider_mappings if x.item_id == item_id) + file_item = await self.resolve(item_id) + + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=prov_mapping.audio_format, + media_type=MediaType.TRACK, + stream_type=StreamType.LOCAL_FILE, + duration=library_item.duration, + size=file_item.file_size, + data=file_item, + path=file_item.absolute_path, + can_seek=True, + ) + + async def resolve_image(self, path: str) -> str | bytes: + """ + Resolve an image from an image path. + + This either returns (a generator to get) raw bytes of the image or + a string with an http(s) URL or local path that is accessible from the server. + """ + file_item = await self.resolve(path) + return file_item.absolute_path + + async def _parse_track( + self, file_item: FileSystemItem, full_album_metadata: bool = False + ) -> Track: + """Get full track details by id.""" + # ruff: noqa: PLR0915, PLR0912 + + # parse tags + tags = await parse_tags(file_item.absolute_path, file_item.file_size) + name, version = parse_title_and_version(tags.title, tags.version) + track = Track( + item_id=file_item.path, + provider=self.instance_id, + name=name, + sort_name=tags.title_sort, + version=version, + provider_mappings={ + ProviderMapping( + item_id=file_item.path, + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(tags.format), + sample_rate=tags.sample_rate, + bit_depth=tags.bits_per_sample, + channels=tags.channels, + bit_rate=tags.bit_rate, + ), + details=file_item.checksum, + ) + }, + disc_number=tags.disc or 0, + track_number=tags.track or 0, + ) + + if isrc_tags := tags.isrc: + for isrsc in isrc_tags: + track.external_ids.add((ExternalID.ISRC, isrsc)) + + if acoustid := tags.get("acoustidid"): + track.external_ids.add((ExternalID.ACOUSTID, acoustid)) + + # album + album = track.album = ( + await self._parse_album(track_path=file_item.path, track_tags=tags) + if tags.album + else None + ) + + # track artist(s) + for index, track_artist_str in enumerate(tags.artists): + # prefer album artist if match + if album and ( + album_artist_match := next( + (x for x in album.artists if x.name == track_artist_str), None + ) + ): + track.artists.append(album_artist_match) + continue + artist = await self._parse_artist( + track_artist_str, + sort_name=( + tags.artist_sort_names[index] if index < len(tags.artist_sort_names) else None + ), + mbid=( + tags.musicbrainz_artistids[index] + if index < len(tags.musicbrainz_artistids) + else None + ), + ) + track.artists.append(artist) + + # handle embedded cover image + if tags.has_cover_image: + # we do not actually embed the image in the metadata because that would consume too + # much space and bandwidth. Instead we set the filename as value so the image can + # be retrieved later in realtime. + track.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=file_item.path, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + ) + + # copy (embedded) album image from track (if the album itself doesn't have an image) + if album and not album.image and track.image: + album.metadata.images = UniqueList([track.image]) + + # parse other info + track.duration = int(tags.duration or 0) + track.metadata.genres = set(tags.genres) + if tags.disc: + track.disc_number = tags.disc + if tags.track: + track.track_number = tags.track + track.metadata.copyright = tags.get("copyright") + track.metadata.lyrics = tags.lyrics + explicit_tag = tags.get("itunesadvisory") + if explicit_tag is not None: + track.metadata.explicit = explicit_tag == "1" + if tags.musicbrainz_recordingid: + track.mbid = tags.musicbrainz_recordingid + track.metadata.chapters = UniqueList(tags.chapters) + # handle (optional) loudness measurement tag(s) + if tags.track_loudness is not None: + await self.mass.music.set_loudness( + track.item_id, self.instance_id, tags.track_loudness, tags.track_album_loudness + ) + return track + + async def _parse_artist( + self, + name: str, + album_dir: str | None = None, + sort_name: str | None = None, + mbid: str | None = None, + artist_path: str | None = None, + ) -> Artist: + """Parse full (album) Artist.""" + if not artist_path: + # we need to hunt for the artist (metadata) path on disk + # this can either be relative to the album path or at root level + # check if we have an artist folder for this artist at root level + safe_artist_name = create_safe_string(name, lowercase=False, replace_space=False) + if await self.exists(name): + artist_path = name + elif await self.exists(safe_artist_name): + artist_path = safe_artist_name + elif album_dir and (foldermatch := get_artist_dir(name, album_dir=album_dir)): + # try to find (album)artist folder based on album path + artist_path = foldermatch + else: + # check if we have an existing item to retrieve the artist path + async for item in self.mass.music.artists.iter_library_items(search=name): + if not compare_strings(name, item.name): + continue + for prov_mapping in item.provider_mappings: + if prov_mapping.provider_instance != self.instance_id: + continue + if prov_mapping.url: + artist_path = prov_mapping.url + break + if artist_path: + break + + # prefer (short lived) cache for a bit more speed + cache_base_key = f"{self.instance_id}.artist" + if artist_path and (cache := await self.cache.get(artist_path, base_key=cache_base_key)): + return cast(Artist, cache) + + prov_artist_id = artist_path or name + artist = Artist( + item_id=prov_artist_id, + provider=self.instance_id, + name=name, + sort_name=sort_name, + provider_mappings={ + ProviderMapping( + item_id=prov_artist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=artist_path, + ) + }, + ) + if mbid: + artist.mbid = mbid + if not artist_path: + return artist + + # grab additional metadata within the Artist's folder + nfo_file = os.path.join(artist_path, "artist.nfo") + if await self.exists(nfo_file): + # found NFO file with metadata + # https://kodi.wiki/view/NFO_files/Artists + nfo_file = self.get_absolute_path(nfo_file) + async with aiofiles.open(nfo_file) as _file: + data = await _file.read() + info = await asyncio.to_thread(xmltodict.parse, data) + info = info["artist"] + artist.name = info.get("title", info.get("name", name)) + if sort_name := info.get("sortname"): + artist.sort_name = sort_name + if mbid := info.get("musicbrainzartistid"): + artist.mbid = mbid + if description := info.get("biography"): + artist.metadata.description = description + if genre := info.get("genre"): + artist.metadata.genres = set(split_items(genre)) + # find local images + if images := await self._get_local_images(artist_path): + artist.metadata.images = UniqueList(images) + + await self.cache.set(artist_path, artist, base_key=cache_base_key, expiration=120) + + return artist + + async def _parse_album(self, track_path: str, track_tags: AudioTags) -> Album: + """Parse Album metadata from Track tags.""" + assert track_tags.album + # work out if we have an album and/or disc folder + # track_dir is the folder level where the tracks are located + # this may be a separate disc folder (Disc 1, Disc 2 etc) underneath the album folder + # or this is an album folder with the disc attached + track_dir = os.path.dirname(track_path) + album_dir = get_album_dir(track_dir, track_tags.album) + + cache_base_key = f"{self.instance_id}.album" + if album_dir and (cache := await self.cache.get(album_dir, base_key=cache_base_key)): + return cast(Album, cache) + + # album artist(s) + album_artists: UniqueList[Artist | ItemMapping] = UniqueList() + if track_tags.album_artists: + for index, album_artist_str in enumerate(track_tags.album_artists): + artist = await self._parse_artist( + album_artist_str, + album_dir=album_dir, + sort_name=( + track_tags.album_artist_sort_names[index] + if index < len(track_tags.album_artist_sort_names) + else None + ), + mbid=( + track_tags.musicbrainz_albumartistids[index] + if index < len(track_tags.musicbrainz_albumartistids) + else None + ), + ) + album_artists.append(artist) + else: + # album artist tag is missing, determine fallback + fallback_action = self.config.get_value(CONF_MISSING_ALBUM_ARTIST_ACTION) + if fallback_action == "folder_name" and album_dir: + possible_artist_folder = os.path.dirname(album_dir) + self.logger.warning( + "%s is missing ID3 tag [albumartist], using foldername %s as fallback", + track_path, + possible_artist_folder, + ) + album_artist_str = possible_artist_folder.rsplit(os.sep)[-1] + album_artists = UniqueList( + [await self._parse_artist(name=album_artist_str, album_dir=album_dir)] + ) + # fallback to track artists (if defined by user) + elif fallback_action == "track_artist": + self.logger.warning( + "%s is missing ID3 tag [albumartist], using track artist(s) as fallback", + track_path, + ) + album_artists = UniqueList( + [ + await self._parse_artist(name=track_artist_str, album_dir=album_dir) + for track_artist_str in track_tags.artists + ] + ) + # all other: fallback to various artists + else: + self.logger.warning( + "%s is missing ID3 tag [albumartist], using %s as fallback", + track_path, + VARIOUS_ARTISTS_NAME, + ) + album_artists = UniqueList( + [await self._parse_artist(name=VARIOUS_ARTISTS_NAME, mbid=VARIOUS_ARTISTS_MBID)] + ) + + if album_dir: # noqa: SIM108 + # prefer the path as id + item_id = album_dir + else: + # create fake item_id based on artist + album + item_id = album_artists[0].name + os.sep + track_tags.album + + name, version = parse_title_and_version(track_tags.album) + album = Album( + item_id=item_id, + provider=self.instance_id, + name=name, + version=version, + sort_name=track_tags.album_sort, + artists=album_artists, + provider_mappings={ + ProviderMapping( + item_id=item_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=album_dir, + ) + }, + ) + if track_tags.barcode: + album.external_ids.add((ExternalID.BARCODE, track_tags.barcode)) + + if track_tags.musicbrainz_albumid: + album.mbid = track_tags.musicbrainz_albumid + if track_tags.musicbrainz_releasegroupid: + album.add_external_id(ExternalID.MB_RELEASEGROUP, track_tags.musicbrainz_releasegroupid) + if track_tags.year: + album.year = track_tags.year + album.album_type = track_tags.album_type + + # hunt for additional metadata and images in the folder structure + if not album_dir: + return album + + for folder_path in (track_dir, album_dir): + if not folder_path or not await self.exists(folder_path): + continue + nfo_file = os.path.join(folder_path, "album.nfo") + if await self.exists(nfo_file): + # found NFO file with metadata + # https://kodi.wiki/view/NFO_files/Artists + nfo_file = self.get_absolute_path(nfo_file) + async with aiofiles.open(nfo_file) as _file: + data = await _file.read() + info = await asyncio.to_thread(xmltodict.parse, data) + info = info["album"] + album.name = info.get("title", info.get("name", name)) + if sort_name := info.get("sortname"): + album.sort_name = sort_name + if releasegroup_id := info.get("musicbrainzreleasegroupid"): + album.add_external_id(ExternalID.MB_RELEASEGROUP, releasegroup_id) + if album_id := info.get("musicbrainzalbumid"): + album.add_external_id(ExternalID.MB_ALBUM, album_id) + if mb_artist_id := info.get("musicbrainzalbumartistid"): + if album.artists and not album.artists[0].mbid: + album.artists[0].mbid = mb_artist_id + if description := info.get("review"): + album.metadata.description = description + if year := info.get("year"): + album.year = int(year) + if genre := info.get("genre"): + album.metadata.genres = set(split_items(genre)) + # parse name/version + album.name, album.version = parse_title_and_version(album.name) + # find local images + if images := await self._get_local_images(folder_path): + if album.metadata.images is None: + album.metadata.images = UniqueList(images) + else: + album.metadata.images += images + await self.cache.set(album_dir, album, base_key=cache_base_key, expiration=120) + return album + + async def _get_local_images(self, folder: str) -> UniqueList[MediaItemImage]: + """Return local images found in a given folderpath.""" + images: UniqueList[MediaItemImage] = UniqueList() + async for item in self.listdir(folder): + if "." not in item.path or item.is_dir: + continue + for ext in IMAGE_EXTENSIONS: + if item.ext != ext: + continue + # try match on filename = one of our imagetypes + if item.name in ImageType: + images.append( + MediaItemImage( + type=ImageType(item.name), + path=item.path, + provider=self.instance_id, + remotely_accessible=False, + ) + ) + continue + # try alternative names for thumbs + for filename in ("folder", "cover", "albumart", "artist"): + if item.name.lower().startswith(filename): + images.append( + MediaItemImage( + type=ImageType.THUMB, + path=item.path, + provider=self.instance_id, + remotely_accessible=False, + ) + ) + break + return images + + async def check_write_access(self) -> None: + """Perform check if we have write access.""" + # verify write access to determine we have playlist create/edit support + # overwrite with provider specific implementation if needed + temp_file_name = self.get_absolute_path(f"{shortuuid.random(8)}.txt") + try: + async with aiofiles.open(temp_file_name, "w") as _file: + await _file.write("test") + await asyncio.to_thread(os.remove, temp_file_name) + self.write_access = True + except Exception as err: + self.logger.debug("Write access disabled: %s", str(err)) + + async def listdir( + self, path: str, recursive: bool = False, sort: bool = False + ) -> AsyncGenerator[FileSystemItem, None]: + """List contents of a given provider directory/path. + + Parameters + ---------- + - path: path of the directory (relative or absolute) to list contents of. + Empty string for provider's root. + - recursive: If True will recursively keep unwrapping subdirectories (scandir equivalent). + + Returns: + ------- + AsyncGenerator yielding FileSystemItem objects. + + """ + abs_path = self.get_absolute_path(path) + for entry in await asyncio.to_thread(sorted_scandir, self.base_path, abs_path, sort): + if recursive and entry.is_dir: + try: + async for subitem in self.listdir(entry.absolute_path, True, sort): + yield subitem + except (OSError, PermissionError) as err: + self.logger.warning("Skip folder %s: %s", entry.path, str(err)) + else: + yield entry + + async def resolve( + self, + file_path: str, + ) -> FileSystemItem: + """Resolve (absolute or relative) path to FileSystemItem.""" + absolute_path = self.get_absolute_path(file_path) + + def _create_item() -> FileSystemItem: + stat = os.stat(absolute_path, follow_symlinks=False) + return FileSystemItem( + filename=os.path.basename(file_path), + path=get_relative_path(self.base_path, file_path), + absolute_path=absolute_path, + is_dir=os.path.isdir(absolute_path), + is_file=os.path.isfile(absolute_path), + checksum=str(int(stat.st_mtime)), + file_size=stat.st_size, + ) + + # run in thread because strictly taken this may be blocking IO + return await asyncio.to_thread(_create_item) + + async def exists(self, file_path: str) -> bool: + """Return bool is this FileSystem musicprovider has given file/dir.""" + if not file_path: + return False # guard + abs_path = self.get_absolute_path(file_path) + return bool(await exists(abs_path)) + + def get_absolute_path(self, file_path: str) -> str: + """Return absolute path for given file path.""" + return get_absolute_path(self.base_path, file_path) diff --git a/music_assistant/providers/filesystem_local/helpers.py b/music_assistant/providers/filesystem_local/helpers.py new file mode 100644 index 00000000..57e87b93 --- /dev/null +++ b/music_assistant/providers/filesystem_local/helpers.py @@ -0,0 +1,153 @@ +"""Some helpers for Filesystem based Musicproviders.""" + +from __future__ import annotations + +import os +import re +from dataclasses import dataclass + +from music_assistant.helpers.compare import compare_strings + +IGNORE_DIRS = ("recycle", "Recently-Snaphot") + + +@dataclass +class FileSystemItem: + """Representation of an item (file or directory) on the filesystem. + + - filename: Name (not path) of the file (or directory). + - path: Relative path to the item on this filesystem provider. + - absolute_path: Absolute path to this item. + - is_file: Boolean if item is file (not directory or symlink). + - is_dir: Boolean if item is directory (not file). + - checksum: Checksum for this path (usually last modified time). + - file_size : File size in number of bytes or None if unknown (or not a file). + """ + + filename: str + path: str + absolute_path: str + is_file: bool + is_dir: bool + checksum: str + file_size: int | None = None + + @property + def ext(self) -> str | None: + """Return file extension.""" + try: + return self.filename.rsplit(".", 1)[1] + except IndexError: + return None + + @property + def name(self) -> str: + """Return file name (without extension).""" + return self.filename.rsplit(".", 1)[0] + + +def get_artist_dir( + artist_name: str, + album_dir: str | None, +) -> str | None: + """Look for (Album)Artist directory in path of a track (or album).""" + if not album_dir: + return None + parentdir = os.path.dirname(album_dir) + # account for disc or album sublevel by ignoring (max) 2 levels if needed + matched_dir: str | None = None + for _ in range(3): + dirname = parentdir.rsplit(os.sep)[-1] + if compare_strings(artist_name, dirname, False): + # literal match + # we keep hunting further down to account for the + # edge case where the album name has the same name as the artist + matched_dir = parentdir + parentdir = os.path.dirname(parentdir) + return matched_dir + + +def get_album_dir(track_dir: str, album_name: str) -> str | None: + """Return album/parent directory of a track.""" + parentdir = track_dir + # account for disc sublevel by ignoring 1 level if needed + for _ in range(2): + dirname = parentdir.rsplit(os.sep)[-1] + if compare_strings(album_name, dirname, False): + # literal match + return parentdir + if compare_strings(album_name, dirname.split(" - ")[-1], False): + # account for ArtistName - AlbumName format in the directory name + return parentdir + if compare_strings(album_name, dirname.split(" - ")[-1].split("(")[0], False): + # account for ArtistName - AlbumName (Version) format in the directory name + return parentdir + if compare_strings(album_name.split("(")[0], dirname, False): + # account for AlbumName (Version) format in the album name + return parentdir + if compare_strings(album_name.split("(")[0], dirname.split(" - ")[-1], False): + # account for ArtistName - AlbumName (Version) format + return parentdir + if len(album_name) > 8 and album_name in dirname: + # dirname contains album name + # (could potentially lead to false positives, hence the length check) + return parentdir + parentdir = os.path.dirname(parentdir) + return None + + +def get_relative_path(base_path: str, path: str) -> str: + """Return the relative path string for a path.""" + if path.startswith(base_path): + path = path.split(base_path)[1] + for sep in ("/", "\\"): + if path.startswith(sep): + path = path[1:] + return path + + +def get_absolute_path(base_path: str, path: str) -> str: + """Return the absolute path string for a path.""" + if path.startswith(base_path): + return path + return os.path.join(base_path, path) + + +def sorted_scandir(base_path: str, sub_path: str, sort: bool = False) -> list[FileSystemItem]: + """ + Implement os.scandir that returns (optionally) sorted entries. + + Not async friendly! + """ + + def nat_key(name: str) -> tuple[int | str, ...]: + """Sort key for natural sorting.""" + return tuple(int(s) if s.isdigit() else s for s in re.split(r"(\d+)", name)) + + def create_item(entry: os.DirEntry) -> FileSystemItem: + """Create FileSystemItem from os.DirEntry.""" + absolute_path = get_absolute_path(base_path, entry.path) + stat = entry.stat(follow_symlinks=False) + return FileSystemItem( + filename=entry.name, + path=get_relative_path(base_path, entry.path), + absolute_path=absolute_path, + is_file=entry.is_file(follow_symlinks=False), + is_dir=entry.is_dir(follow_symlinks=False), + checksum=str(int(stat.st_mtime)), + file_size=stat.st_size, + ) + + items = [ + create_item(x) + for x in os.scandir(sub_path) + # filter out invalid dirs and hidden files + if x.name not in IGNORE_DIRS and not x.name.startswith(".") + ] + if sort: + return sorted( + items, + # sort by (natural) name + key=lambda x: nat_key(x.name), + ) + return items diff --git a/music_assistant/providers/filesystem_local/manifest.json b/music_assistant/providers/filesystem_local/manifest.json new file mode 100644 index 00000000..7c3ea523 --- /dev/null +++ b/music_assistant/providers/filesystem_local/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "music", + "domain": "filesystem_local", + "name": "Filesystem (local disk)", + "description": "Support for music files that are present on a local accessible disk/folder.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/music-providers/filesystem/", + "multi_instance": true, + "icon": "harddisk" +} diff --git a/music_assistant/providers/filesystem_smb/__init__.py b/music_assistant/providers/filesystem_smb/__init__.py new file mode 100644 index 00000000..b43245dd --- /dev/null +++ b/music_assistant/providers/filesystem_smb/__init__.py @@ -0,0 +1,230 @@ +"""SMB filesystem provider for Music Assistant.""" + +from __future__ import annotations + +import os +import platform +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import LoginFailed + +from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME, VERBOSE_LOG_LEVEL +from music_assistant.helpers.process import check_output +from music_assistant.helpers.util import get_ip_from_host +from music_assistant.providers.filesystem_local import ( + CONF_ENTRY_MISSING_ALBUM_ARTIST, + LocalFileSystemProvider, + exists, + makedirs, +) + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +CONF_HOST = "host" +CONF_SHARE = "share" +CONF_SUBFOLDER = "subfolder" +CONF_MOUNT_OPTIONS = "mount_options" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + # check if valid dns name is given for the host + server = str(config.get_value(CONF_HOST)) + if not await get_ip_from_host(server): + msg = f"Unable to resolve {server}, make sure the address is resolveable." + raise LoginFailed(msg) + # check if share is valid + share = str(config.get_value(CONF_SHARE)) + if not share or "/" in share or "\\" in share: + msg = "Invalid share name" + raise LoginFailed(msg) + return SMBFileSystemProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_HOST, + type=ConfigEntryType.STRING, + label="Server", + required=True, + description="The (fqdn) hostname of the SMB/CIFS/DFS server to connect to." + "For example mynas.local.", + ), + ConfigEntry( + key=CONF_SHARE, + type=ConfigEntryType.STRING, + label="Share", + required=True, + description="The name of the share/service you'd like to connect to on " + "the remote host, For example 'media'.", + ), + ConfigEntry( + key=CONF_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + default_value="guest", + description="The username to authenticate to the remote server. " + "For anynymous access you may want to try with the user `guest`.", + ), + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password", + required=False, + default_value=None, + description="The username to authenticate to the remote server. " + "For anynymous access you may want to try with the user `guest`.", + ), + ConfigEntry( + key=CONF_SUBFOLDER, + type=ConfigEntryType.STRING, + label="Subfolder", + required=False, + default_value="", + description="[optional] Use if your music is stored in a sublevel of the share. " + "E.g. 'collections' or 'albums/A-K'.", + ), + ConfigEntry( + key=CONF_MOUNT_OPTIONS, + type=ConfigEntryType.STRING, + label="Mount options", + required=False, + category="advanced", + default_value="noserverino,file_mode=0775,dir_mode=0775,uid=0,gid=0", + description="[optional] Any additional mount options you " + "want to pass to the mount command if needed for your particular setup.", + ), + CONF_ENTRY_MISSING_ALBUM_ARTIST, + ) + + +class SMBFileSystemProvider(LocalFileSystemProvider): + """ + Implementation of an SMB File System Provider. + + Basically this is just a wrapper around the regular local files provider, + except for the fact that it will mount a remote folder to a temporary location. + We went for this OS-depdendent approach because there is no solid async-compatible + smb library for Python (and we tried both pysmb and smbprotocol). + """ + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + # base_path will be the path where we're going to mount the remote share + self.base_path = f"/tmp/{self.instance_id}" # noqa: S108 + if not await exists(self.base_path): + await makedirs(self.base_path) + + try: + # do unmount first to cleanup any unexpected state + await self.unmount(ignore_error=True) + await self.mount() + except Exception as err: + msg = f"Connection failed for the given details: {err}" + raise LoginFailed(msg) from err + + await self.check_write_access() + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + await self.unmount() + + async def mount(self) -> None: + """Mount the SMB location to a temporary folder.""" + server = str(self.config.get_value(CONF_HOST)) + username = str(self.config.get_value(CONF_USERNAME)) + password = self.config.get_value(CONF_PASSWORD) + share = str(self.config.get_value(CONF_SHARE)) + + # handle optional subfolder + subfolder = str(self.config.get_value(CONF_SUBFOLDER)) + if subfolder: + subfolder = subfolder.replace("\\", "/") + if not subfolder.startswith("/"): + subfolder = "/" + subfolder + if subfolder.endswith("/"): + subfolder = subfolder[:-1] + + if platform.system() == "Darwin": + # NOTE: MacOS does not support special characters in the username/password + password_str = f":{password}" if password else "" + mount_cmd = [ + "mount", + "-t", + "smbfs", + f"//{username}{password_str}@{server}/{share}{subfolder}", + self.base_path, + ] + + elif platform.system() == "Linux": + options = ["rw"] + if mount_options := str(self.config.get_value(CONF_MOUNT_OPTIONS)): + options += mount_options.split(",") + options_str = ",".join(options) + + # pass the username+password using (scoped) env variables + # to prevent leaking in the process list and special chars supported + env_vars = { + **os.environ, + "USER": username, + } + if password: + env_vars["PASSWD"] = str(password) + + mount_cmd = [ + "mount", + "-t", + "cifs", + "-o", + options_str, + f"//{server}/{share}{subfolder}", + self.base_path, + ] + else: + msg = f"SMB provider is not supported on {platform.system()}" + raise LoginFailed(msg) + + self.logger.debug("Mounting //%s/%s%s to %s", server, share, subfolder, self.base_path) + self.logger.log( + VERBOSE_LOG_LEVEL, + "Using mount command: %s", + " ".join(mount_cmd), + ) + returncode, output = await check_output(*mount_cmd, env=env_vars) + if returncode != 0: + msg = f"SMB mount failed with error: {output.decode()}" + raise LoginFailed(msg) + + async def unmount(self, ignore_error: bool = False) -> None: + """Unmount the remote share.""" + returncode, output = await check_output("umount", self.base_path) + if returncode != 0 and not ignore_error: + self.logger.warning("SMB unmount failed with error: %s", output.decode()) diff --git a/music_assistant/providers/filesystem_smb/manifest.json b/music_assistant/providers/filesystem_smb/manifest.json new file mode 100644 index 00000000..53bc716e --- /dev/null +++ b/music_assistant/providers/filesystem_smb/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "music", + "domain": "filesystem_smb", + "name": "Filesystem (remote share)", + "description": "Support for music files that are present on remote SMB/CIFS.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/music-providers/filesystem/", + "multi_instance": true, + "icon": "network" + } diff --git a/music_assistant/providers/fully_kiosk/__init__.py b/music_assistant/providers/fully_kiosk/__init__.py new file mode 100644 index 00000000..4cbee92f --- /dev/null +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -0,0 +1,214 @@ +"""FullyKiosk Player provider for Music Assistant.""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import TYPE_CHECKING + +from fullykiosk import FullyKiosk +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, + CONF_ENTRY_FLOW_MODE_ENFORCED, + ConfigEntry, + ConfigValueType, +) +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError, SetupFailedError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.constants import ( + CONF_ENFORCE_MP3, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_PORT, + VERBOSE_LOG_LEVEL, +) +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +AUDIOMANAGER_STREAM_MUSIC = 3 + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return FullyKioskProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_IP_ADDRESS, + type=ConfigEntryType.STRING, + label="IP-Address (or hostname) of the device running Fully Kiosk/app.", + required=True, + ), + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password to use to connect to the Fully Kiosk API.", + required=True, + ), + ConfigEntry( + key=CONF_PORT, + type=ConfigEntryType.STRING, + default_value="2323", + label="Port to use to connect to the Fully Kiosk API (default is 2323).", + required=True, + category="advanced", + ), + ) + + +class FullyKioskProvider(PlayerProvider): + """Player provider for FullyKiosk based players.""" + + _fully: FullyKiosk + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + # set-up fullykiosk logging + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("fullykiosk").setLevel(logging.DEBUG) + else: + logging.getLogger("fullykiosk").setLevel(self.logger.level + 10) + self._fully = FullyKiosk( + self.mass.http_session, + self.config.get_value(CONF_IP_ADDRESS), + self.config.get_value(CONF_PORT), + self.config.get_value(CONF_PASSWORD), + ) + try: + async with asyncio.timeout(15): + await self._fully.getDeviceInfo() + except Exception as err: + msg = f"Unable to start the FullyKiosk connection ({err!s}" + raise SetupFailedError(msg) from err + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + # Add FullyKiosk device to Player controller. + player_id = self._fully.deviceInfo["deviceID"] + player = self.mass.players.get(player_id, raise_unavailable=False) + address = ( + f"http://{self.config.get_value(CONF_IP_ADDRESS)}:{self.config.get_value(CONF_PORT)}" + ) + if not player: + player = Player( + player_id=player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=self._fully.deviceInfo["deviceName"], + available=True, + powered=False, + device_info=DeviceInfo( + model=self._fully.deviceInfo["deviceModel"], + manufacturer=self._fully.deviceInfo["deviceManufacturer"], + address=address, + ), + supported_features=(PlayerFeature.VOLUME_SET,), + needs_poll=True, + poll_interval=10, + ) + await self.mass.players.register_or_update(player) + self._handle_player_update() + + def _handle_player_update(self) -> None: + """Update FullyKiosk player attributes.""" + player_id = self._fully.deviceInfo["deviceID"] + if not (player := self.mass.players.get(player_id)): + return + player.name = self._fully.deviceInfo["deviceName"] + # player.volume_level = snap_client.volume + for volume_dict in self._fully.deviceInfo.get("audioVolumes", []): + if str(AUDIOMANAGER_STREAM_MUSIC) in volume_dict: + volume = volume_dict[str(AUDIOMANAGER_STREAM_MUSIC)] + player.volume_level = volume + break + current_url = self._fully.deviceInfo.get("soundUrlPlaying") + player.current_item_id = current_url + if not current_url: + player.state = PlayerState.IDLE + player.available = True + self.mass.players.update(player_id) + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + base_entries = await super().get_player_config_entries(player_id) + return ( + *base_entries, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, + ) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + if not (player := self.mass.players.get(player_id, raise_unavailable=False)): + return + await self._fully.setAudioVolume(volume_level, AUDIOMANAGER_STREAM_MUSIC) + player.volume_level = volume_level + self.mass.players.update(player_id) + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + if not (player := self.mass.players.get(player_id, raise_unavailable=False)): + return + await self._fully.stopSound() + player.state = PlayerState.IDLE + self.mass.players.update(player_id) + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + if not (player := self.mass.players.get(player_id)): + return + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): + media.uri = media.uri.replace(".flac", ".mp3") + await self._fully.playSound(media.uri, AUDIOMANAGER_STREAM_MUSIC) + player.current_media = media + player.elapsed_time = 0 + player.elapsed_time_last_updated = time.time() + player.state = PlayerState.PLAYING + self.mass.players.update(player_id) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + try: + async with asyncio.timeout(15): + await self._fully.getDeviceInfo() + self._handle_player_update() + except Exception as err: + msg = f"Unable to start the FullyKiosk connection ({err!s}" + raise PlayerUnavailableError(msg) from err diff --git a/music_assistant/providers/fully_kiosk/manifest.json b/music_assistant/providers/fully_kiosk/manifest.json new file mode 100644 index 00000000..1059df8e --- /dev/null +++ b/music_assistant/providers/fully_kiosk/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "player", + "domain": "fully_kiosk", + "name": "Fully Kiosk Browser", + "description": "Support for media players from the Fully Kiosk app.", + "codeowners": ["@music-assistant"], + "requirements": ["python-fullykiosk==0.0.14"], + "documentation": "https://music-assistant.io/player-support/fully-kiosk/", + "multi_instance": true, + "builtin": false +} diff --git a/music_assistant/providers/hass/__init__.py b/music_assistant/providers/hass/__init__.py new file mode 100644 index 00000000..dc243eb4 --- /dev/null +++ b/music_assistant/providers/hass/__init__.py @@ -0,0 +1,207 @@ +""" +Home Assistant Plugin for Music Assistant. + +The plugin is the core of all communication to/from Home Assistant and +responsible for maintaining the WebSocket API connection to HA. +Also, the Music Assistant integration within HA will relay its own api +communication over the HA api for more flexibility as well as security. +""" + +from __future__ import annotations + +import asyncio +import logging +from typing import TYPE_CHECKING + +import shortuuid +from hass_client import HomeAssistantClient +from hass_client.exceptions import BaseHassClientError +from hass_client.utils import ( + base_url, + get_auth_url, + get_long_lived_token, + get_token, + get_websocket_url, +) +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import LoginFailed, SetupFailedError + +from music_assistant.constants import MASS_LOGO_ONLINE +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.models.plugin import PluginProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +DOMAIN = "hass" +CONF_URL = "url" +CONF_AUTH_TOKEN = "token" +CONF_ACTION_AUTH = "auth" +CONF_VERIFY_SSL = "verify_ssl" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return HomeAssistant(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # config flow auth action/step (authenticate button clicked) + if action == CONF_ACTION_AUTH: + hass_url = values[CONF_URL] + async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: + client_id = base_url(auth_helper.callback_url) + auth_url = get_auth_url( + hass_url, + auth_helper.callback_url, + client_id=client_id, + state=values["session_id"], + ) + result = await auth_helper.authenticate(auth_url) + if result["state"] != values["session_id"]: + msg = "session id mismatch" + raise LoginFailed(msg) + # get access token after auth was a success + token_details = await get_token(hass_url, result["code"], client_id=client_id) + # register for a long lived token + long_lived_token = await get_long_lived_token( + hass_url, + token_details["access_token"], + client_name=f"Music Assistant {shortuuid.random(6)}", + client_icon=MASS_LOGO_ONLINE, + lifespan=365 * 2, + ) + # set the retrieved token on the values object to pass along + values[CONF_AUTH_TOKEN] = long_lived_token + + if mass.running_as_hass_addon: + # on supervisor, we use the internal url + # token set to None for auto retrieval + return ( + ConfigEntry( + key=CONF_URL, + type=ConfigEntryType.STRING, + label=CONF_URL, + required=True, + default_value="http://supervisor/core/api", + value="http://supervisor/core/api", + hidden=True, + ), + ConfigEntry( + key=CONF_AUTH_TOKEN, + type=ConfigEntryType.STRING, + label=CONF_AUTH_TOKEN, + required=False, + default_value=None, + value=None, + hidden=True, + ), + ConfigEntry( + key=CONF_VERIFY_SSL, + type=ConfigEntryType.BOOLEAN, + label=CONF_VERIFY_SSL, + required=False, + default_value=False, + hidden=True, + ), + ) + # manual configuration + return ( + ConfigEntry( + key=CONF_URL, + type=ConfigEntryType.STRING, + label="URL", + required=True, + description="URL to your Home Assistant instance (e.g. http://192.168.1.1:8123)", + value=values.get(CONF_URL) if values else None, + ), + ConfigEntry( + key=CONF_ACTION_AUTH, + type=ConfigEntryType.ACTION, + label="(re)Authenticate Home Assistant", + description="Authenticate to your home assistant " + "instance and generate the long lived token.", + action=CONF_ACTION_AUTH, + depends_on=CONF_URL, + required=False, + ), + ConfigEntry( + key=CONF_AUTH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Authentication token for HomeAssistant", + description="You can either paste a Long Lived Token here manually or use the " + "'authenticate' button to generate a token for you with logging in.", + depends_on=CONF_URL, + value=values.get(CONF_AUTH_TOKEN) if values else None, + category="advanced", + ), + ConfigEntry( + key=CONF_VERIFY_SSL, + type=ConfigEntryType.BOOLEAN, + label="Verify SSL", + required=False, + description="Whether or not to verify the certificate of SSL/TLS connections.", + category="advanced", + default_value=True, + ), + ) + + +class HomeAssistant(PluginProvider): + """Home Assistant Plugin for Music Assistant.""" + + hass: HomeAssistantClient + _listen_task: asyncio.Task | None = None + + async def handle_async_init(self) -> None: + """Handle async initialization of the plugin.""" + url = get_websocket_url(self.config.get_value(CONF_URL)) + token = self.config.get_value(CONF_AUTH_TOKEN) + logging.getLogger("hass_client").setLevel(self.logger.level + 10) + self.hass = HomeAssistantClient(url, token, self.mass.http_session) + try: + await self.hass.connect(ssl=bool(self.config.get_value(CONF_VERIFY_SSL))) + except BaseHassClientError as err: + err_msg = str(err) or err.__class__.__name__ + raise SetupFailedError(err_msg) from err + self._listen_task = self.mass.create_task(self._hass_listener()) + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + if self._listen_task and not self._listen_task.done(): + self._listen_task.cancel() + await self.hass.disconnect() + + async def _hass_listener(self) -> None: + """Start listening on the HA websockets.""" + try: + # start listening will block until the connection is lost/closed + await self.hass.start_listening() + except BaseHassClientError as err: + self.logger.warning("Connection to HA lost due to error: %s", err) + self.logger.info("Connection to HA lost. Connection will be automatically retried later.") + # schedule a reload of the provider + self.mass.call_later(5, self.mass.load_provider, self.instance_id, allow_retry=True) diff --git a/music_assistant/providers/hass/icon.svg b/music_assistant/providers/hass/icon.svg new file mode 100644 index 00000000..73037fee --- /dev/null +++ b/music_assistant/providers/hass/icon.svg @@ -0,0 +1,5 @@ +<svg viewBox="0 0 240 240" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path d="M240 224.762C240 233.012 233.25 239.762 225 239.762H15C6.75 239.762 0 233.012 0 224.762V134.762C0 126.512 4.77 114.993 10.61 109.153L109.39 10.3725C115.22 4.5425 124.77 4.5425 130.6 10.3725L229.39 109.162C235.22 114.992 240 126.522 240 134.772V224.772V224.762Z" fill="#F2F4F9"/> + <path d="M229.39 109.153L130.61 10.3725C124.78 4.5425 115.23 4.5425 109.4 10.3725L10.61 109.153C4.78 114.983 0 126.512 0 134.762V224.762C0 233.012 6.75 239.762 15 239.762H107.27L66.64 199.132C64.55 199.852 62.32 200.262 60 200.262C48.7 200.262 39.5 191.062 39.5 179.762C39.5 168.462 48.7 159.262 60 159.262C71.3 159.262 80.5 168.462 80.5 179.762C80.5 182.092 80.09 184.322 79.37 186.412L111 218.042V102.162C104.2 98.8225 99.5 91.8425 99.5 83.7725C99.5 72.4725 108.7 63.2725 120 63.2725C131.3 63.2725 140.5 72.4725 140.5 83.7725C140.5 91.8425 135.8 98.8225 129 102.162V183.432L160.46 151.972C159.84 150.012 159.5 147.932 159.5 145.772C159.5 134.472 168.7 125.272 180 125.272C191.3 125.272 200.5 134.472 200.5 145.772C200.5 157.072 191.3 166.272 180 166.272C177.5 166.272 175.12 165.802 172.91 164.982L129 208.892V239.772H225C233.25 239.772 240 233.022 240 224.772V134.772C240 126.522 235.23 115.002 229.39 109.162V109.153Z" fill="#18BCF2"/> +</svg> diff --git a/music_assistant/providers/hass/manifest.json b/music_assistant/providers/hass/manifest.json new file mode 100644 index 00000000..4fee6af8 --- /dev/null +++ b/music_assistant/providers/hass/manifest.json @@ -0,0 +1,12 @@ +{ + "type": "plugin", + "domain": "hass", + "name": "Home Assistant", + "description": "Connect Music Assistant to Home Assistant.", + "codeowners": ["@music-assistant"], + "documentation": "", + "multi_instance": false, + "builtin": false, + "icon": "md:webhook", + "requirements": ["hass-client==1.2.0"] +} diff --git a/music_assistant/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py new file mode 100644 index 00000000..cba0a6c8 --- /dev/null +++ b/music_assistant/providers/hass_players/__init__.py @@ -0,0 +1,484 @@ +""" +Home Assistant PlayerProvider for Music Assistant. + +Allows using media_player entities in HA to be used as players in MA. +Requires the Home Assistant Plugin. +""" + +from __future__ import annotations + +import time +from enum import IntFlag +from typing import TYPE_CHECKING, Any + +from hass_client.exceptions import FailedCommand +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_ENABLE_ICY_METADATA, + CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_HTTP_PROFILE, + ConfigEntry, + ConfigValueOption, + ConfigValueType, +) +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import SetupFailedError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.helpers.datetime import from_iso_string +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.hass import DOMAIN as HASS_DOMAIN + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from hass_client.models import CompressedState, EntityStateEvent + from hass_client.models import Device as HassDevice + from hass_client.models import Entity as HassEntity + from hass_client.models import State as HassState + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + from music_assistant.providers.hass import HomeAssistant as HomeAssistantProvider + +CONF_PLAYERS = "players" + +StateMap = { + "playing": PlayerState.PLAYING, + "paused": PlayerState.PAUSED, + "buffering": PlayerState.PLAYING, + "idle": PlayerState.IDLE, + "off": PlayerState.IDLE, + "standby": PlayerState.IDLE, + "unknown": PlayerState.IDLE, + "unavailable": PlayerState.IDLE, +} + + +class MediaPlayerEntityFeature(IntFlag): + """Supported features of the media player entity.""" + + PAUSE = 1 + SEEK = 2 + VOLUME_SET = 4 + VOLUME_MUTE = 8 + PREVIOUS_TRACK = 16 + NEXT_TRACK = 32 + + TURN_ON = 128 + TURN_OFF = 256 + PLAY_MEDIA = 512 + VOLUME_STEP = 1024 + SELECT_SOURCE = 2048 + STOP = 4096 + CLEAR_PLAYLIST = 8192 + PLAY = 16384 + SHUFFLE_SET = 32768 + SELECT_SOUND_MODE = 65536 + BROWSE_MEDIA = 131072 + REPEAT_SET = 262144 + GROUPING = 524288 + MEDIA_ANNOUNCE = 1048576 + MEDIA_ENQUEUE = 2097152 + + +CONF_ENFORCE_MP3 = "enforce_mp3" + + +PLAYER_CONFIG_ENTRIES = ( + CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, + CONF_ENTRY_HTTP_PROFILE, + CONF_ENTRY_ENABLE_ICY_METADATA, +) + + +async def _get_hass_media_players( + hass_prov: HomeAssistantProvider, +) -> AsyncGenerator[HassState, None]: + """Return all HA state objects for (valid) media_player entities.""" + for state in await hass_prov.hass.get_states(): + if not state["entity_id"].startswith("media_player"): + continue + if "mass_player_id" in state["attributes"]: + # filter out mass players + continue + if "friendly_name" not in state["attributes"]: + # filter out invalid/unavailable players + continue + supported_features = MediaPlayerEntityFeature(state["attributes"]["supported_features"]) + if MediaPlayerEntityFeature.PLAY_MEDIA not in supported_features: + continue + yield state + + +async def _get_hass_media_player( + hass_prov: HomeAssistantProvider, entity_id: str +) -> HassState | None: + """Return Hass state object for a single media_player entity.""" + for state in await hass_prov.hass.get_states(): + if state["entity_id"] == entity_id: + return state + return None + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + hass_prov: HomeAssistantProvider = mass.get_provider(HASS_DOMAIN) + if not hass_prov: + msg = "The Home Assistant Plugin needs to be set-up first" + raise SetupFailedError(msg) + prov = HomeAssistantPlayers(mass, manifest, config) + prov.hass_prov = hass_prov + return prov + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + hass_prov: HomeAssistantProvider = mass.get_provider(HASS_DOMAIN) + player_entities: list[ConfigValueOption] = [] + if hass_prov and hass_prov.hass.connected: + async for state in _get_hass_media_players(hass_prov): + name = f'{state["attributes"]["friendly_name"]} ({state["entity_id"]})' + player_entities.append(ConfigValueOption(name, state["entity_id"])) + return ( + ConfigEntry( + key=CONF_PLAYERS, + type=ConfigEntryType.STRING, + label="Player entities", + required=True, + options=tuple(player_entities), + multi_value=True, + description="Specify which HA media_player entity id's you " + "like to import as players in Music Assistant.", + ), + ) + + +class HomeAssistantPlayers(PlayerProvider): + """Home Assistant PlayerProvider for Music Assistant.""" + + hass_prov: HomeAssistantProvider + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + await super().loaded_in_mass() + player_ids: list[str] = self.config.get_value(CONF_PLAYERS) + # prefetch the device- and entity registry + device_registry = {x["id"]: x for x in await self.hass_prov.hass.get_device_registry()} + entity_registry = { + x["entity_id"]: x for x in await self.hass_prov.hass.get_entity_registry() + } + # setup players from hass entities + async for state in _get_hass_media_players(self.hass_prov): + if state["entity_id"] not in player_ids: + continue + await self._setup_player(state, entity_registry, device_registry) + # register for entity state updates + await self.hass_prov.hass.subscribe_entities(self._on_entity_state_update, player_ids) + # remove any leftover players (after reconfigure of players) + for player in self.players: + if player.player_id not in player_ids: + self.mass.players.remove(player.player_id) + + async def get_player_config_entries( + self, + player_id: str, + ) -> tuple[ConfigEntry, ...]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + entries = await super().get_player_config_entries(player_id) + entries = entries + PLAYER_CONFIG_ENTRIES + if hass_state := await _get_hass_media_player(self.hass_prov, player_id): + hass_supported_features = MediaPlayerEntityFeature( + hass_state["attributes"]["supported_features"] + ) + if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in hass_supported_features: + entries += (CONF_ENTRY_FLOW_MODE_ENFORCED,) + + return entries + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player. + + - player_id: player_id of the player to handle the command. + """ + try: + await self.hass_prov.hass.call_service( + domain="media_player", service="media_stop", target={"entity_id": player_id} + ) + except FailedCommand as exc: + # some HA players do not support STOP + if "does not support this service" not in str(exc): + raise + if player := self.mass.players.get(player_id): + if PlayerFeature.PAUSE in player.supported_features: + await self.cmd_pause(player_id) + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY (unpause) command to given player. + + - player_id: player_id of the player to handle the command. + """ + await self.hass_prov.hass.call_service( + domain="media_player", service="media_play", target={"entity_id": player_id} + ) + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player. + + - player_id: player_id of the player to handle the command. + """ + await self.hass_prov.hass.call_service( + domain="media_player", + service="media_pause", + target={"entity_id": player_id}, + ) + + async def play_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle PLAY MEDIA on given player.""" + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): + media.uri = media.uri.replace(".flac", ".mp3") + await self.hass_prov.hass.call_service( + domain="media_player", + service="play_media", + service_data={ + "media_content_id": media.uri, + "media_content_type": "music", + "enqueue": "replace", + "extra": { + "metadata": { + "title": media.title, + "artist": media.artist, + "metadataType": 3, + "album": media.album, + "albumName": media.album, + "duration": media.duration, + "images": [{"url": media.image_url}] if media.image_url else None, + "imageUrl": media.image_url, + } + }, + }, + target={"entity_id": player_id}, + ) + # optimistically set the elapsed_time as some HA players do not report this + if player := self.mass.players.get(player_id): + player.elapsed_time = 0 + player.elapsed_time_last_updated = time.time() + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next queue item on the player.""" + if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): + media.uri = media.uri.replace(".flac", ".mp3") + await self.hass_prov.hass.call_service( + domain="media_player", + service="play_media", + service_data={ + "media_content_id": media.uri, + "media_content_type": "music", + "enqueue": "next", + }, + target={"entity_id": player_id}, + ) + + async def cmd_power(self, player_id: str, powered: bool) -> None: + """Send POWER command to given player. + + - player_id: player_id of the player to handle the command. + - powered: bool if player should be powered on or off. + """ + await self.hass_prov.hass.call_service( + domain="media_player", + service="turn_on" if powered else "turn_off", + target={"entity_id": player_id}, + ) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player. + + - player_id: player_id of the player to handle the command. + - volume_level: volume level (0..100) to set on the player. + """ + await self.hass_prov.hass.call_service( + domain="media_player", + service="volume_set", + service_data={"volume_level": volume_level / 100}, + target={"entity_id": player_id}, + ) + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player. + + - player_id: player_id of the player to handle the command. + - muted: bool if player should be muted. + """ + await self.hass_prov.hass.call_service( + domain="media_player", + service="volume_mute", + service_data={"is_volume_muted": muted}, + target={"entity_id": player_id}, + ) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup master or group player. + """ + # NOTE: not in use yet, as we do not support syncgroups in MA for HA players + await self.hass_prov.hass.call_service( + domain="media_player", + service="join", + service_data={"group_members": [player_id]}, + target={"entity_id": target_player}, + ) + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + # NOTE: not in use yet, as we do not support syncgroups in MA for HA players + await self.hass_prov.hass.call_service( + domain="media_player", + service="unjoin", + target={"entity_id": player_id}, + ) + + async def _setup_player( + self, + state: HassState, + entity_registry: dict[str, HassEntity], + device_registry: dict[str, HassDevice], + ) -> None: + """Handle setup of a Player from an hass entity.""" + hass_device: HassDevice | None = None + if entity_registry_entry := entity_registry.get(state["entity_id"]): + hass_device = device_registry.get(entity_registry_entry["device_id"]) + hass_supported_features = MediaPlayerEntityFeature( + state["attributes"]["supported_features"] + ) + supported_features: list[PlayerFeature] = [] + if MediaPlayerEntityFeature.PAUSE in hass_supported_features: + supported_features.append(PlayerFeature.PAUSE) + if MediaPlayerEntityFeature.VOLUME_SET in hass_supported_features: + supported_features.append(PlayerFeature.VOLUME_SET) + if MediaPlayerEntityFeature.VOLUME_MUTE in hass_supported_features: + supported_features.append(PlayerFeature.VOLUME_MUTE) + if MediaPlayerEntityFeature.MEDIA_ENQUEUE in hass_supported_features: + supported_features.append(PlayerFeature.ENQUEUE) + if ( + MediaPlayerEntityFeature.TURN_ON in hass_supported_features + and MediaPlayerEntityFeature.TURN_OFF in hass_supported_features + ): + supported_features.append(PlayerFeature.POWER) + player = Player( + player_id=state["entity_id"], + provider=self.instance_id, + type=PlayerType.PLAYER, + name=state["attributes"]["friendly_name"], + available=state["state"] not in ("unavailable", "unknown"), + powered=state["state"] not in ("unavailable", "unknown", "standby", "off"), + device_info=DeviceInfo( + model=hass_device["model"] if hass_device else "Unknown model", + manufacturer=( + hass_device["manufacturer"] if hass_device else "Unknown Manufacturer" + ), + ), + supported_features=tuple(supported_features), + state=StateMap.get(state["state"], PlayerState.IDLE), + ) + self._update_player_attributes(player, state["attributes"]) + await self.mass.players.register_or_update(player) + + def _on_entity_state_update(self, event: EntityStateEvent) -> None: + """Handle Entity State event.""" + + def update_player_from_state_msg(entity_id: str, state: CompressedState) -> None: + """Handle updating MA player with updated info in a HA CompressedState.""" + player = self.mass.players.get(entity_id) + if player is None: + # edge case - one of our subscribed entities was not available at startup + # and now came available - we should still set it up + player_ids: list[str] = self.config.get_value(CONF_PLAYERS) + if entity_id not in player_ids: + return # should not happen, but guard just in case + self.mass.create_task(self._late_add_player(entity_id)) + return + if "s" in state: + player.state = StateMap.get(state["s"], PlayerState.IDLE) + player.powered = state["s"] not in ( + "unavailable", + "unknown", + "standby", + "off", + ) + if "a" in state: + self._update_player_attributes(player, state["a"]) + self.mass.players.update(entity_id) + + if entity_additions := event.get("a"): + for entity_id, state in entity_additions.items(): + update_player_from_state_msg(entity_id, state) + if entity_changes := event.get("c"): + for entity_id, state_diff in entity_changes.items(): + if "+" not in state_diff: + continue + update_player_from_state_msg(entity_id, state_diff["+"]) + + def _update_player_attributes(self, player: Player, attributes: dict[str, Any]) -> None: + """Update Player attributes from HA state attributes.""" + for key, value in attributes.items(): + if key == "media_position": + player.elapsed_time = value + if key == "media_position_updated_at": + player.elapsed_time_last_updated = from_iso_string(value).timestamp() + if key == "volume_level": + player.volume_level = int(value * 100) + if key == "volume_muted": + player.volume_muted = value + if key == "media_content_id": + player.current_item_id = value + if key == "group_members": + if value and value[0] == player.player_id: + player.group_childs = value + player.synced_to = None + elif value and value[0] != player.player_id: + player.group_childs = set() + player.synced_to = value[0] + else: + player.group_childs = set() + player.synced_to = None + + async def _late_add_player(self, entity_id: str) -> None: + """Handle setup of Player from HA entity that became available after startup.""" + # prefetch the device- and entity registry + device_registry = {x["id"]: x for x in await self.hass_prov.hass.get_device_registry()} + entity_registry = { + x["entity_id"]: x for x in await self.hass_prov.hass.get_entity_registry() + } + async for state in _get_hass_media_players(self.hass_prov): + if state["entity_id"] != entity_id: + continue + await self._setup_player(state, entity_registry, device_registry) diff --git a/music_assistant/providers/hass_players/icon.svg b/music_assistant/providers/hass_players/icon.svg new file mode 100644 index 00000000..73037fee --- /dev/null +++ b/music_assistant/providers/hass_players/icon.svg @@ -0,0 +1,5 @@ +<svg viewBox="0 0 240 240" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path d="M240 224.762C240 233.012 233.25 239.762 225 239.762H15C6.75 239.762 0 233.012 0 224.762V134.762C0 126.512 4.77 114.993 10.61 109.153L109.39 10.3725C115.22 4.5425 124.77 4.5425 130.6 10.3725L229.39 109.162C235.22 114.992 240 126.522 240 134.772V224.772V224.762Z" fill="#F2F4F9"/> + <path d="M229.39 109.153L130.61 10.3725C124.78 4.5425 115.23 4.5425 109.4 10.3725L10.61 109.153C4.78 114.983 0 126.512 0 134.762V224.762C0 233.012 6.75 239.762 15 239.762H107.27L66.64 199.132C64.55 199.852 62.32 200.262 60 200.262C48.7 200.262 39.5 191.062 39.5 179.762C39.5 168.462 48.7 159.262 60 159.262C71.3 159.262 80.5 168.462 80.5 179.762C80.5 182.092 80.09 184.322 79.37 186.412L111 218.042V102.162C104.2 98.8225 99.5 91.8425 99.5 83.7725C99.5 72.4725 108.7 63.2725 120 63.2725C131.3 63.2725 140.5 72.4725 140.5 83.7725C140.5 91.8425 135.8 98.8225 129 102.162V183.432L160.46 151.972C159.84 150.012 159.5 147.932 159.5 145.772C159.5 134.472 168.7 125.272 180 125.272C191.3 125.272 200.5 134.472 200.5 145.772C200.5 157.072 191.3 166.272 180 166.272C177.5 166.272 175.12 165.802 172.91 164.982L129 208.892V239.772H225C233.25 239.772 240 233.022 240 224.772V134.772C240 126.522 235.23 115.002 229.39 109.162V109.153Z" fill="#18BCF2"/> +</svg> diff --git a/music_assistant/providers/hass_players/manifest.json b/music_assistant/providers/hass_players/manifest.json new file mode 100644 index 00000000..3c0fae3e --- /dev/null +++ b/music_assistant/providers/hass_players/manifest.json @@ -0,0 +1,13 @@ +{ + "type": "player", + "domain": "hass_players", + "name": "Home Assistant MediaPlayers", + "description": "Use (supported) Home Assistant media players as players in Music Assistant.", + "codeowners": ["@music-assistant"], + "documentation": "https://music-assistant.io/player-support/ha/", + "multi_instance": false, + "builtin": false, + "icon": "md:webhook", + "depends_on": "hass", + "requirements": [] +} diff --git a/music_assistant/providers/jellyfin/__init__.py b/music_assistant/providers/jellyfin/__init__.py new file mode 100644 index 00000000..f6a03ed9 --- /dev/null +++ b/music_assistant/providers/jellyfin/__init__.py @@ -0,0 +1,504 @@ +"""Jellyfin support for MusicAssistant.""" + +from __future__ import annotations + +import mimetypes +import socket +import uuid +from asyncio import TaskGroup +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING + +from aiojellyfin import MediaLibrary as JellyMediaLibrary +from aiojellyfin import NotFound, SessionConfiguration, authenticate_by_name +from aiojellyfin import Track as JellyTrack +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant import MusicAssistant +from music_assistant.constants import UNKNOWN_ARTIST_ID_MBID +from music_assistant.models import ProviderInstanceType +from music_assistant.models.music_provider import MusicProvider +from music_assistant.providers.jellyfin.parsers import ( + parse_album, + parse_artist, + parse_playlist, + parse_track, +) + +from .const import ( + ALBUM_FIELDS, + ARTIST_FIELDS, + CLIENT_VERSION, + ITEM_KEY_COLLECTION_TYPE, + ITEM_KEY_ID, + ITEM_KEY_MEDIA_CHANNELS, + ITEM_KEY_MEDIA_CODEC, + ITEM_KEY_MEDIA_SOURCES, + ITEM_KEY_MEDIA_STREAMS, + ITEM_KEY_NAME, + ITEM_KEY_RUNTIME_TICKS, + SUPPORTED_CONTAINER_FORMATS, + TRACK_FIELDS, + UNKNOWN_ARTIST_MAPPING, + USER_APP_NAME, +) + +if TYPE_CHECKING: + from music_assistant_models.provider import ProviderManifest + +CONF_URL = "url" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" +CONF_VERIFY_SSL = "verify_ssl" +FAKE_ARTIST_PREFIX = "_fake://" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return JellyfinProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # config flow auth action/step (authenticate button clicked) + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_URL, + type=ConfigEntryType.STRING, + label="Server", + required=True, + description="The url of the Jellyfin server to connect to.", + ), + ConfigEntry( + key=CONF_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + description="The username to authenticate to the remote server." + "the remote host, For example 'media'.", + ), + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password", + required=False, + description="The password to authenticate to the remote server.", + ), + ConfigEntry( + key=CONF_VERIFY_SSL, + type=ConfigEntryType.BOOLEAN, + label="Verify SSL", + required=False, + description="Whether or not to verify the certificate of SSL/TLS connections.", + category="advanced", + default_value=True, + ), + ) + + +class JellyfinProvider(MusicProvider): + """Provider for a jellyfin music library.""" + + async def handle_async_init(self) -> None: + """Initialize provider(instance) with given configuration.""" + session_config = SessionConfiguration( + session=self.mass.http_session, + url=str(self.config.get_value(CONF_URL)), + verify_ssl=bool(self.config.get_value(CONF_VERIFY_SSL)), + app_name=USER_APP_NAME, + app_version=CLIENT_VERSION, + device_name=socket.gethostname(), + device_id=str(uuid.uuid4()), + ) + + try: + self._client = await authenticate_by_name( + session_config, + str(self.config.get_value(CONF_USERNAME)), + str(self.config.get_value(CONF_PASSWORD)), + ) + except Exception as err: + raise LoginFailed(f"Authentication failed: {err}") from err + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return a list of supported features.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.SIMILAR_TRACKS, + ) + + @property + def is_streaming_provider(self) -> bool: + """Return True if the provider is a streaming provider.""" + return False + + async def _search_track(self, search_query: str, limit: int) -> list[Track]: + resultset = ( + await self._client.tracks.search_term(search_query) + .limit(limit) + .enable_userdata() + .fields(*TRACK_FIELDS) + .request() + ) + tracks = [] + for item in resultset["Items"]: + tracks.append(parse_track(self.logger, self.instance_id, self._client, item)) + return tracks + + async def _search_album(self, search_query: str, limit: int) -> list[Album]: + if "-" in search_query: + searchterms = search_query.split(" - ") + albumname = searchterms[1] + else: + albumname = search_query + resultset = ( + await self._client.albums.search_term(albumname) + .limit(limit) + .enable_userdata() + .fields(*ALBUM_FIELDS) + .request() + ) + albums = [] + for item in resultset["Items"]: + albums.append(parse_album(self.logger, self.instance_id, self._client, item)) + return albums + + async def _search_artist(self, search_query: str, limit: int) -> list[Artist]: + resultset = ( + await self._client.artists.search_term(search_query) + .limit(limit) + .enable_userdata() + .fields(*ARTIST_FIELDS) + .request() + ) + artists = [] + for item in resultset["Items"]: + artists.append(parse_artist(self.logger, self.instance_id, self._client, item)) + return artists + + async def _search_playlist(self, search_query: str, limit: int) -> list[Playlist]: + resultset = ( + await self._client.playlists.search_term(search_query) + .limit(limit) + .enable_userdata() + .request() + ) + playlists = [] + for item in resultset["Items"]: + playlists.append(parse_playlist(self.instance_id, self._client, item)) + return playlists + + async def search( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 20, + ) -> SearchResults: + """Perform search on the plex library. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + artists = None + albums = None + tracks = None + playlists = None + + async with TaskGroup() as tg: + if MediaType.ARTIST in media_types: + artists = tg.create_task(self._search_artist(search_query, limit)) + if MediaType.ALBUM in media_types: + albums = tg.create_task(self._search_album(search_query, limit)) + if MediaType.TRACK in media_types: + tracks = tg.create_task(self._search_track(search_query, limit)) + if MediaType.PLAYLIST in media_types: + playlists = tg.create_task(self._search_playlist(search_query, limit)) + + search_results = SearchResults() + + if artists: + search_results.artists = artists.result() + if albums: + search_results.albums = albums.result() + if tracks: + search_results.tracks = tracks.result() + if playlists: + search_results.playlists = playlists.result() + + return search_results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Jellyfin Music.""" + jellyfin_libraries = await self._get_music_libraries() + for jellyfin_library in jellyfin_libraries: + stream = ( + self._client.artists.parent(jellyfin_library[ITEM_KEY_ID]) + .enable_userdata() + .fields(*ARTIST_FIELDS) + .stream(100) + ) + async for artist in stream: + yield parse_artist(self.logger, self.instance_id, self._client, artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Jellyfin Music.""" + jellyfin_libraries = await self._get_music_libraries() + for jellyfin_library in jellyfin_libraries: + stream = ( + self._client.albums.parent(jellyfin_library[ITEM_KEY_ID]) + .enable_userdata() + .fields(*ALBUM_FIELDS) + .stream(100) + ) + async for album in stream: + yield parse_album(self.logger, self.instance_id, self._client, album) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Jellyfin Music.""" + jellyfin_libraries = await self._get_music_libraries() + for jellyfin_library in jellyfin_libraries: + stream = ( + self._client.tracks.parent(jellyfin_library[ITEM_KEY_ID]) + .enable_userdata() + .fields(*TRACK_FIELDS) + .stream(100) + ) + async for track in stream: + if not len(track[ITEM_KEY_MEDIA_STREAMS]): + self.logger.warning( + "Invalid track %s: Does not have any media streams", track[ITEM_KEY_NAME] + ) + continue + yield parse_track(self.logger, self.instance_id, self._client, track) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + playlist_libraries = await self._get_playlists() + for playlist_library in playlist_libraries: + stream = ( + self._client.playlists.parent(playlist_library[ITEM_KEY_ID]) + .enable_userdata() + .stream(100) + ) + async for playlist in stream: + if "MediaType" in playlist: # Only jellyfin has this property + if playlist["MediaType"] == "Audio": + yield parse_playlist(self.instance_id, self._client, playlist) + else: # emby playlists are only audio type + yield parse_playlist(self.instance_id, self._client, playlist) + + async def get_album(self, prov_album_id: str) -> Album: + """Get full album details by id.""" + try: + album = await self._client.get_album(prov_album_id) + except NotFound: + raise MediaNotFoundError(f"Item {prov_album_id} not found") + return parse_album(self.logger, self.instance_id, self._client, album) + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + jellyfin_album_tracks = ( + await self._client.tracks.parent(prov_album_id) + .enable_userdata() + .fields(*TRACK_FIELDS) + .request() + ) + return [ + parse_track(self.logger, self.instance_id, self._client, jellyfin_album_track) + for jellyfin_album_track in jellyfin_album_tracks["Items"] + ] + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + if prov_artist_id == UNKNOWN_ARTIST_MAPPING.item_id: + artist = Artist( + item_id=UNKNOWN_ARTIST_MAPPING.item_id, + name=UNKNOWN_ARTIST_MAPPING.name, + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=UNKNOWN_ARTIST_MAPPING.item_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + artist.mbid = UNKNOWN_ARTIST_ID_MBID + return artist + + try: + jellyfin_artist = await self._client.get_artist(prov_artist_id) + except NotFound: + raise MediaNotFoundError(f"Item {prov_artist_id} not found") + return parse_artist(self.logger, self.instance_id, self._client, jellyfin_artist) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + try: + track = await self._client.get_track(prov_track_id) + except NotFound: + raise MediaNotFoundError(f"Item {prov_track_id} not found") + return parse_track(self.logger, self.instance_id, self._client, track) + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get full playlist details by id.""" + try: + playlist = await self._client.get_playlist(prov_playlist_id) + except NotFound: + raise MediaNotFoundError(f"Item {prov_playlist_id} not found") + return parse_playlist(self.instance_id, self._client, playlist) + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # paging not supported, we always return the whole list at once + return [] + # TODO: Does Jellyfin support paging here? + jellyfin_playlist = await self._client.get_playlist(prov_playlist_id) + playlist_items = ( + await self._client.tracks.parent(jellyfin_playlist[ITEM_KEY_ID]) + .enable_userdata() + .fields(*TRACK_FIELDS) + .request() + ) + for index, jellyfin_track in enumerate(playlist_items["Items"], 1): + try: + if track := parse_track( + self.logger, self.instance_id, self._client, jellyfin_track + ): + if not track.position: + track.position = index + result.append(track) + except (KeyError, ValueError) as err: + self.logger.error( + "Skipping track %s: %s", jellyfin_track.get(ITEM_KEY_NAME, index), str(err) + ) + return result + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get a list of albums for the given artist.""" + if not prov_artist_id.startswith(FAKE_ARTIST_PREFIX): + return [] + albums = ( + await self._client.albums.parent(prov_artist_id) + .fields(*ALBUM_FIELDS) + .enable_userdata() + .request() + ) + return [ + parse_album(self.logger, self.instance_id, self._client, album) + for album in albums["Items"] + ] + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + jellyfin_track = await self._client.get_track(item_id) + mimetype = self._media_mime_type(jellyfin_track) + media_stream = jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0] + url = self._client.audio_url(jellyfin_track[ITEM_KEY_ID], SUPPORTED_CONTAINER_FORMATS) + if ITEM_KEY_MEDIA_CODEC in media_stream: + content_type = ContentType.try_parse(media_stream[ITEM_KEY_MEDIA_CODEC]) + else: + content_type = ContentType.try_parse(mimetype) if mimetype else ContentType.UNKNOWN + return StreamDetails( + item_id=jellyfin_track[ITEM_KEY_ID], + provider=self.instance_id, + audio_format=AudioFormat( + content_type=content_type, + channels=jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0][ITEM_KEY_MEDIA_CHANNELS], + ), + stream_type=StreamType.HTTP, + duration=int( + jellyfin_track[ITEM_KEY_RUNTIME_TICKS] / 10000000 + ), # 10000000 ticks per millisecond) + path=url, + ) + + async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + resp = await self._client.get_similar_tracks( + prov_track_id, limit=limit, fields=TRACK_FIELDS + ) + return [ + parse_track(self.logger, self.instance_id, self._client, track) + for track in resp["Items"] + ] + + async def _get_music_libraries(self) -> list[JellyMediaLibrary]: + """Return all supported libraries a user has access to.""" + response = await self._client.get_media_folders() + libraries = response["Items"] + result = [] + for library in libraries: + if ITEM_KEY_COLLECTION_TYPE in library and library[ITEM_KEY_COLLECTION_TYPE] in "music": + result.append(library) + return result + + async def _get_playlists(self) -> list[JellyMediaLibrary]: + """Return all supported libraries a user has access to.""" + response = await self._client.get_media_folders() + libraries = response["Items"] + result = [] + for library in libraries: + if ( + ITEM_KEY_COLLECTION_TYPE in library + and library[ITEM_KEY_COLLECTION_TYPE] in "playlists" + ): + result.append(library) + return result + + def _media_mime_type(self, media_item: JellyTrack) -> str | None: + """Return the mime type of a media item.""" + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): + return None + + media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if "Path" not in media_source: + return None + + path = media_source["Path"] + mime_type, _ = mimetypes.guess_type(path) + + return mime_type diff --git a/music_assistant/providers/jellyfin/const.py b/music_assistant/providers/jellyfin/const.py new file mode 100644 index 00000000..ea660c54 --- /dev/null +++ b/music_assistant/providers/jellyfin/const.py @@ -0,0 +1,99 @@ +"""Constants for the Jellyfin integration.""" + +from typing import Final + +from aiojellyfin import ImageType as JellyImageType +from aiojellyfin import ItemFields +from music_assistant_models.enums import ImageType, MediaType +from music_assistant_models.media_items import ItemMapping + +from music_assistant.constants import UNKNOWN_ARTIST + +DOMAIN: Final = "jellyfin" + +CLIENT_VERSION: Final = "0.1" + +COLLECTION_TYPE_MOVIES: Final = "movies" +COLLECTION_TYPE_MUSIC: Final = "music" +COLLECTION_TYPE_TVSHOWS: Final = "tvshows" + +CONF_CLIENT_DEVICE_ID: Final = "client_device_id" + +DEFAULT_NAME: Final = "Jellyfin" + +ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType" +ITEM_KEY_ID: Final = "Id" +ITEM_KEY_IMAGE_TAGS: Final = "ImageTags" +ITEM_KEY_INDEX_NUMBER: Final = "IndexNumber" +ITEM_KEY_MEDIA_SOURCES: Final = "MediaSources" +ITEM_KEY_MEDIA_TYPE: Final = "MediaType" +ITEM_KEY_MEDIA_STREAMS: Final = "MediaStreams" +ITEM_KEY_MEDIA_CHANNELS: Final = "Channels" +ITEM_KEY_MEDIA_CODEC: Final = "Codec" +ITEM_KEY_NAME: Final = "Name" +ITEM_KEY_PROVIDER_IDS: Final = "ProviderIds" +ITEM_KEY_PRODUCTION_YEAR: Final = "ProductionYear" +ITEM_KEY_OVERVIEW: Final = "Overview" +ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP: Final = "MusicBrainzReleaseGroup" +ITEM_KEY_MUSICBRAINZ_ARTIST: Final = "MusicBrainzArtist" +ITEM_KEY_MUSICBRAINZ_ALBUM: Final = "MusicBrainzAlbum" +ITEM_KEY_MUSICBRAINZ_TRACK: Final = "MusicBrainzTrack" +ITEM_KEY_SORT_NAME: Final = "SortName" +ITEM_KEY_ALBUM_ARTIST: Final = "AlbumArtist" +ITEM_KEY_ALBUM_ARTISTS: Final = "AlbumArtists" +ITEM_KEY_ALBUM: Final = "Album" +ITEM_KEY_ALBUM_ID: Final = "AlbumId" +ITEM_KEY_ARTIST_ITEMS: Final = "ArtistItems" +ITEM_KEY_CAN_DOWNLOAD: Final = "CanDownload" +ITEM_KEY_PARENT_INDEX_NUM: Final = "ParentIndexNumber" +ITEM_KEY_RUNTIME_TICKS: Final = "RunTimeTicks" +ITEM_KEY_USER_DATA: Final = "UserData" + +ITEM_TYPE_AUDIO: Final = "Audio" +ITEM_TYPE_LIBRARY: Final = "CollectionFolder" + +USER_DATA_KEY_IS_FAVORITE: Final = "IsFavorite" + +MAX_IMAGE_WIDTH: Final = 500 +MAX_STREAMING_BITRATE: Final = "140000000" + +MEDIA_SOURCE_KEY_PATH: Final = "Path" + +MEDIA_TYPE_AUDIO: Final = "Audio" +MEDIA_TYPE_NONE: Final = "" + +SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC] + +SUPPORTED_CONTAINER_FORMATS: Final = "ogg,flac,mp3,aac,mpeg,alac,wav,aiff,wma,m4a,m4b,dsf,opus,wv" + +PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO] + +ARTIST_FIELDS: Final = [ + ItemFields.Overview, + ItemFields.ProviderIds, + ItemFields.SortName, +] +ALBUM_FIELDS: Final = [ + ItemFields.Overview, + ItemFields.ProviderIds, + ItemFields.SortName, +] +TRACK_FIELDS: Final = [ + ItemFields.ProviderIds, + ItemFields.CanDownload, + ItemFields.SortName, + ItemFields.MediaSources, + ItemFields.MediaStreams, +] + +USER_APP_NAME: Final = "Music Assistant" +USER_AGENT: Final = "Music-Assistant-1.0" + +UNKNOWN_ARTIST_MAPPING: Final = ItemMapping( + media_type=MediaType.ARTIST, item_id=UNKNOWN_ARTIST, provider=DOMAIN, name=UNKNOWN_ARTIST +) + +MEDIA_IMAGE_TYPES: Final = { + JellyImageType.Primary: ImageType.THUMB, + JellyImageType.Logo: ImageType.LOGO, +} diff --git a/music_assistant/providers/jellyfin/icon.svg b/music_assistant/providers/jellyfin/icon.svg new file mode 100644 index 00000000..87a6dd39 --- /dev/null +++ b/music_assistant/providers/jellyfin/icon.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- ***** BEGIN LICENSE BLOCK ***** + - Part of the Jellyfin project (https://jellyfin.media) + - + - All copyright belongs to the Jellyfin contributors; a full list can + - be found in the file CONTRIBUTORS.md + - + - This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. + - To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/. +- ***** END LICENSE BLOCK ***** --> +<svg id="icon-solid-black" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"> + <defs> + <linearGradient id="linear-gradient" x1="110.25" y1="213.3" x2="496.14" y2="436.09" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#aa5cc3"/> + <stop offset="1" stop-color="#00a4dc"/> + </linearGradient> + </defs> + <g id="icon-solid"> + <path id="inner-shape" d="M256,201.62c-20.44,0-86.23,119.29-76.2,139.43s142.48,19.92,152.4,0S276.47,201.63,256,201.62Z" fill="url(#linear-gradient)"/> + <path id="outer-shape" d="M256,23.3C194.44,23.3-3.82,382.73,26.41,443.43s429.34,60,459.24,0S317.62,23.3,256,23.3ZM406.51,390.76c-19.59,39.33-281.08,39.77-300.89,0S215.71,115.48,256.06,115.48,426.1,351.42,406.51,390.76Z" fill="url(#linear-gradient)"/> + </g> +</svg> diff --git a/music_assistant/providers/jellyfin/manifest.json b/music_assistant/providers/jellyfin/manifest.json new file mode 100644 index 00000000..cf9cca56 --- /dev/null +++ b/music_assistant/providers/jellyfin/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "jellyfin", + "name": "Jellyfin Media Server Library", + "description": "Support for the Jellyfin streaming provider in Music Assistant.", + "codeowners": ["@lokiberra", "@Jc2k"], + "requirements": ["aiojellyfin==0.10.1"], + "documentation": "https://music-assistant.io/music-providers/jellyfin/", + "multi_instance": true +} diff --git a/music_assistant/providers/jellyfin/parsers.py b/music_assistant/providers/jellyfin/parsers.py new file mode 100644 index 00000000..96545118 --- /dev/null +++ b/music_assistant/providers/jellyfin/parsers.py @@ -0,0 +1,305 @@ +"""Parse Jellyfin metadata into Music Assistant models.""" + +from __future__ import annotations + +import logging +from logging import Logger +from typing import TYPE_CHECKING + +from aiojellyfin import ImageType as JellyImageType +from music_assistant_models.enums import ContentType, ExternalID, ImageType, MediaType +from music_assistant_models.errors import InvalidDataError +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + ItemMapping, + MediaItemImage, + Playlist, + ProviderMapping, + Track, + UniqueList, +) + +from .const import ( + DOMAIN, + ITEM_KEY_ALBUM, + ITEM_KEY_ALBUM_ARTIST, + ITEM_KEY_ALBUM_ARTISTS, + ITEM_KEY_ALBUM_ID, + ITEM_KEY_ARTIST_ITEMS, + ITEM_KEY_CAN_DOWNLOAD, + ITEM_KEY_ID, + ITEM_KEY_IMAGE_TAGS, + ITEM_KEY_MEDIA_CODEC, + ITEM_KEY_MEDIA_STREAMS, + ITEM_KEY_MUSICBRAINZ_ALBUM, + ITEM_KEY_MUSICBRAINZ_ARTIST, + ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP, + ITEM_KEY_MUSICBRAINZ_TRACK, + ITEM_KEY_NAME, + ITEM_KEY_OVERVIEW, + ITEM_KEY_PARENT_INDEX_NUM, + ITEM_KEY_PRODUCTION_YEAR, + ITEM_KEY_PROVIDER_IDS, + ITEM_KEY_RUNTIME_TICKS, + ITEM_KEY_SORT_NAME, + ITEM_KEY_USER_DATA, + MEDIA_IMAGE_TYPES, + UNKNOWN_ARTIST_MAPPING, + USER_DATA_KEY_IS_FAVORITE, +) + +if TYPE_CHECKING: + from aiojellyfin import Album as JellyAlbum + from aiojellyfin import Artist as JellyArtist + from aiojellyfin import Connection + from aiojellyfin import MediaItem as JellyMediaItem + from aiojellyfin import Playlist as JellyPlaylist + from aiojellyfin import Track as JellyTrack + + +def parse_album( + logger: Logger, instance_id: str, connection: Connection, jellyfin_album: JellyAlbum +) -> Album: + """Parse a Jellyfin Album response to an Album model object.""" + album_id = jellyfin_album[ITEM_KEY_ID] + album = Album( + item_id=album_id, + provider=DOMAIN, + name=jellyfin_album[ITEM_KEY_NAME], + provider_mappings={ + ProviderMapping( + item_id=str(album_id), + provider_domain=DOMAIN, + provider_instance=instance_id, + ) + }, + ) + if ITEM_KEY_PRODUCTION_YEAR in jellyfin_album: + album.year = jellyfin_album[ITEM_KEY_PRODUCTION_YEAR] + album.metadata.images = _get_artwork(instance_id, connection, jellyfin_album) + if ITEM_KEY_OVERVIEW in jellyfin_album: + album.metadata.description = jellyfin_album[ITEM_KEY_OVERVIEW] + if ITEM_KEY_MUSICBRAINZ_ALBUM in jellyfin_album[ITEM_KEY_PROVIDER_IDS]: + try: + album.add_external_id( + ExternalID.MB_ALBUM, + jellyfin_album[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_ALBUM], + ) + except InvalidDataError as error: + logger.warning( + "Jellyfin has an invalid musicbrainz album id for album %s", + album.name, + exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, + ) + if ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP in jellyfin_album[ITEM_KEY_PROVIDER_IDS]: + try: + album.add_external_id( + ExternalID.MB_RELEASEGROUP, + jellyfin_album[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP], + ) + except InvalidDataError as error: + logger.warning( + "Jellyfin has an invalid musicbrainz id for album %s", + album.name, + exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, + ) + if ITEM_KEY_SORT_NAME in jellyfin_album: + album.sort_name = jellyfin_album[ITEM_KEY_SORT_NAME] + if ITEM_KEY_ALBUM_ARTIST in jellyfin_album: + for album_artist in jellyfin_album[ITEM_KEY_ALBUM_ARTISTS]: + album.artists.append( + ItemMapping( + media_type=MediaType.ARTIST, + item_id=album_artist[ITEM_KEY_ID], + provider=instance_id, + name=album_artist[ITEM_KEY_NAME], + ) + ) + elif len(jellyfin_album.get(ITEM_KEY_ARTIST_ITEMS, [])) >= 1: + for artist_item in jellyfin_album[ITEM_KEY_ARTIST_ITEMS]: + album.artists.append( + ItemMapping( + media_type=MediaType.ARTIST, + item_id=artist_item[ITEM_KEY_ID], + provider=instance_id, + name=artist_item[ITEM_KEY_NAME], + ) + ) + else: + album.artists.append(UNKNOWN_ARTIST_MAPPING) + + user_data = jellyfin_album.get(ITEM_KEY_USER_DATA, {}) + album.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) + return album + + +def parse_artist( + logger: Logger, instance_id: str, connection: Connection, jellyfin_artist: JellyArtist +) -> Artist: + """Parse a Jellyfin Artist response to Artist model object.""" + artist_id = jellyfin_artist[ITEM_KEY_ID] + artist = Artist( + item_id=artist_id, + name=jellyfin_artist[ITEM_KEY_NAME], + provider=DOMAIN, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=DOMAIN, + provider_instance=instance_id, + ) + }, + ) + if ITEM_KEY_OVERVIEW in jellyfin_artist: + artist.metadata.description = jellyfin_artist[ITEM_KEY_OVERVIEW] + if ITEM_KEY_MUSICBRAINZ_ARTIST in jellyfin_artist[ITEM_KEY_PROVIDER_IDS]: + try: + artist.mbid = jellyfin_artist[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_ARTIST] + except InvalidDataError as error: + logger.warning( + "Jellyfin has an invalid musicbrainz id for artist %s", + artist.name, + exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, + ) + if ITEM_KEY_SORT_NAME in jellyfin_artist: + artist.sort_name = jellyfin_artist[ITEM_KEY_SORT_NAME] + artist.metadata.images = _get_artwork(instance_id, connection, jellyfin_artist) + user_data = jellyfin_artist.get(ITEM_KEY_USER_DATA, {}) + artist.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) + return artist + + +def parse_track( + logger: Logger, instance_id: str, client: Connection, jellyfin_track: JellyTrack +) -> Track: + """Parse a Jellyfin Track response to a Track model object.""" + available = False + content = None + available = jellyfin_track[ITEM_KEY_CAN_DOWNLOAD] + content = jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0][ITEM_KEY_MEDIA_CODEC] + track = Track( + item_id=jellyfin_track[ITEM_KEY_ID], + provider=instance_id, + name=jellyfin_track[ITEM_KEY_NAME], + provider_mappings={ + ProviderMapping( + item_id=jellyfin_track[ITEM_KEY_ID], + provider_domain=DOMAIN, + provider_instance=instance_id, + available=available, + audio_format=AudioFormat( + content_type=( + ContentType.try_parse(content) if content else ContentType.UNKNOWN + ), + ), + url=client.audio_url(jellyfin_track[ITEM_KEY_ID]), + ) + }, + ) + + track.disc_number = jellyfin_track.get(ITEM_KEY_PARENT_INDEX_NUM, 0) + track.track_number = jellyfin_track.get("IndexNumber", 0) + if track.track_number is not None and track.track_number >= 0: + track.position = track.track_number + + track.metadata.images = _get_artwork(instance_id, client, jellyfin_track) + + if jellyfin_track[ITEM_KEY_ARTIST_ITEMS]: + for artist_item in jellyfin_track[ITEM_KEY_ARTIST_ITEMS]: + track.artists.append( + ItemMapping( + media_type=MediaType.ARTIST, + item_id=artist_item[ITEM_KEY_ID], + provider=instance_id, + name=artist_item[ITEM_KEY_NAME], + ) + ) + else: + track.artists.append(UNKNOWN_ARTIST_MAPPING) + + if ITEM_KEY_ALBUM_ID in jellyfin_track: + if not (album_name := jellyfin_track.get(ITEM_KEY_ALBUM)): + logger.debug("Track %s has AlbumID but no AlbumName", track.name) + album_name = f"Unknown Album ({jellyfin_track[ITEM_KEY_ALBUM_ID]})" + track.album = ItemMapping( + media_type=MediaType.ALBUM, + item_id=jellyfin_track[ITEM_KEY_ALBUM_ID], + provider=instance_id, + name=album_name, + ) + + if ITEM_KEY_RUNTIME_TICKS in jellyfin_track: + track.duration = int( + jellyfin_track[ITEM_KEY_RUNTIME_TICKS] / 10000000 + ) # 10000000 ticks per millisecond + if ITEM_KEY_MUSICBRAINZ_TRACK in jellyfin_track[ITEM_KEY_PROVIDER_IDS]: + track_mbid = jellyfin_track[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_TRACK] + try: + track.mbid = track_mbid + except InvalidDataError as error: + logger.warning( + "Jellyfin has an invalid musicbrainz id for track %s", + track.name, + exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, + ) + user_data = jellyfin_track.get(ITEM_KEY_USER_DATA, {}) + track.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) + return track + + +def parse_playlist( + instance_id: str, client: Connection, jellyfin_playlist: JellyPlaylist +) -> Playlist: + """Parse a Jellyfin Playlist response to a Playlist object.""" + playlistid = jellyfin_playlist[ITEM_KEY_ID] + playlist = Playlist( + item_id=playlistid, + provider=DOMAIN, + name=jellyfin_playlist[ITEM_KEY_NAME], + provider_mappings={ + ProviderMapping( + item_id=playlistid, + provider_domain=DOMAIN, + provider_instance=instance_id, + ) + }, + ) + if ITEM_KEY_OVERVIEW in jellyfin_playlist: + playlist.metadata.description = jellyfin_playlist[ITEM_KEY_OVERVIEW] + playlist.metadata.images = _get_artwork(instance_id, client, jellyfin_playlist) + user_data = jellyfin_playlist.get(ITEM_KEY_USER_DATA, {}) + playlist.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) + playlist.is_editable = False + return playlist + + +def _get_artwork( + instance_id: str, client: Connection, media_item: JellyMediaItem +) -> UniqueList[MediaItemImage]: + images: UniqueList[MediaItemImage] = UniqueList() + + for i, _ in enumerate(media_item.get("BackdropImageTags", [])): + images.append( + MediaItemImage( + type=ImageType.FANART, + path=client.artwork(media_item[ITEM_KEY_ID], JellyImageType.Backdrop, index=i), + provider=instance_id, + remotely_accessible=False, + ) + ) + + image_tags = media_item[ITEM_KEY_IMAGE_TAGS] + for jelly_image_type, image_type in MEDIA_IMAGE_TYPES.items(): + if jelly_image_type in image_tags: + images.append( + MediaItemImage( + type=image_type, + path=client.artwork(media_item[ITEM_KEY_ID], jelly_image_type), + provider=instance_id, + remotely_accessible=False, + ) + ) + + return images diff --git a/music_assistant/providers/musicbrainz/__init__.py b/music_assistant/providers/musicbrainz/__init__.py new file mode 100644 index 00000000..95918ad5 --- /dev/null +++ b/music_assistant/providers/musicbrainz/__init__.py @@ -0,0 +1,422 @@ +"""The Musicbrainz Metadata provider for Music Assistant. + +At this time only used for retrieval of ID's but to be expanded to fetch metadata too. +""" + +from __future__ import annotations + +import re +from contextlib import suppress +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +from mashumaro import DataClassDictMixin +from mashumaro.exceptions import MissingField +from music_assistant_models.enums import ExternalID, ProviderFeature +from music_assistant_models.errors import InvalidDataError, ResourceTemporarilyUnavailable + +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.metadata_provider import MetadataProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.media_items import Album, Track + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' + +SUPPORTED_FEATURES = () + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return MusicbrainzProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return () # we do not have any config entries (yet) + + +def replace_hyphens(data: dict[str, Any]) -> dict[str, Any]: + """Change all hyphens to underscores.""" + new_values = {} + for key, value in data.items(): + new_key = key.replace("-", "_") + if isinstance(value, dict): + new_values[new_key] = replace_hyphens(value) + elif isinstance(value, list): + new_values[new_key] = [replace_hyphens(x) if isinstance(x, dict) else x for x in value] + else: + new_values[new_key] = value + return new_values + + +@dataclass +class MusicBrainzTag(DataClassDictMixin): + """Model for a (basic) Tag object as received from the MusicBrainz API.""" + + count: int + name: str + + +@dataclass +class MusicBrainzAlias(DataClassDictMixin): + """Model for a (basic) Alias object from MusicBrainz.""" + + name: str + sort_name: str + + # optional fields + locale: str | None = None + type: str | None = None + primary: bool | None = None + begin_date: str | None = None + end_date: str | None = None + + +@dataclass +class MusicBrainzArtist(DataClassDictMixin): + """Model for a (basic) Artist object from MusicBrainz.""" + + id: str + name: str + sort_name: str + + # optional fields + aliases: list[MusicBrainzAlias] | None = None + tags: list[MusicBrainzTag] | None = None + + +@dataclass +class MusicBrainzArtistCredit(DataClassDictMixin): + """Model for a (basic) ArtistCredit object from MusicBrainz.""" + + name: str + artist: MusicBrainzArtist + + +@dataclass +class MusicBrainzReleaseGroup(DataClassDictMixin): + """Model for a (basic) ReleaseGroup object from MusicBrainz.""" + + id: str + title: str + + # optional fields + primary_type: str | None = None + primary_type_id: str | None = None + secondary_types: list[str] | None = None + secondary_type_ids: list[str] | None = None + artist_credit: list[MusicBrainzArtistCredit] | None = None + + +@dataclass +class MusicBrainzTrack(DataClassDictMixin): + """Model for a (basic) Track object from MusicBrainz.""" + + id: str + number: str + title: str + length: int | None = None + + +@dataclass +class MusicBrainzMedia(DataClassDictMixin): + """Model for a (basic) Media object from MusicBrainz.""" + + format: str + track: list[MusicBrainzTrack] + position: int = 0 + track_count: int = 0 + track_offset: int = 0 + + +@dataclass +class MusicBrainzRelease(DataClassDictMixin): + """Model for a (basic) Release object from MusicBrainz.""" + + id: str + status_id: str + count: int + title: str + status: str + artist_credit: list[MusicBrainzArtistCredit] + release_group: MusicBrainzReleaseGroup + track_count: int = 0 + + # optional fields + media: list[MusicBrainzMedia] = field(default_factory=list) + date: str | None = None + country: str | None = None + disambiguation: str | None = None # version + # TODO (if needed): release-events + + +@dataclass +class MusicBrainzRecording(DataClassDictMixin): + """Model for a (basic) Recording object as received from the MusicBrainz API.""" + + id: str + title: str + artist_credit: list[MusicBrainzArtistCredit] = field(default_factory=list) + # optional fields + length: int | None = None + first_release_date: str | None = None + isrcs: list[str] | None = None + tags: list[MusicBrainzTag] | None = None + disambiguation: str | None = None # version (e.g. live, karaoke etc.) + + +class MusicbrainzProvider(MetadataProvider): + """The Musicbrainz Metadata provider.""" + + throttler = ThrottlerManager(rate_limit=1, period=30) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.cache = self.mass.cache + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def search( + self, artistname: str, albumname: str, trackname: str, trackversion: str | None = None + ) -> tuple[MusicBrainzArtist, MusicBrainzReleaseGroup, MusicBrainzRecording] | None: + """ + Search MusicBrainz details by providing the artist, album and track name. + + NOTE: The MusicBrainz objects returned are simplified objects without the optional data. + """ + trackname, trackversion = parse_title_and_version(trackname, trackversion) + searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname) + searchalbum = re.sub(LUCENE_SPECIAL, r"\\\1", albumname) + searchtracks: list[str] = [] + if trackversion: + searchtracks.append(f"{trackname} ({trackversion})") + searchtracks.append(trackname) + # the version is sometimes appended to the title and sometimes stored + # in disambiguation, so we try both + for strict in (True, False): + for searchtrack in searchtracks: + searchstr = re.sub(LUCENE_SPECIAL, r"\\\1", searchtrack) + result = await self.get_data( + "recording", + query=f'"{searchstr}" AND artist:"{searchartist}" AND release:"{searchalbum}"', + ) + if not result or "recordings" not in result: + continue + for item in result["recordings"]: + # compare track title + if not compare_strings(item["title"], searchtrack, strict): + continue + # compare track version if needed + if ( + trackversion + and trackversion not in searchtrack + and not compare_strings(item.get("disambiguation"), trackversion, strict) + ): + continue + # match (primary) track artist + artist_match: MusicBrainzArtist | None = None + for artist in item["artist-credit"]: + if compare_strings(artist["artist"]["name"], artistname, strict): + artist_match = MusicBrainzArtist.from_dict( + replace_hyphens(artist["artist"]) + ) + else: + for alias in artist["artist"].get("aliases", []): + if compare_strings(alias["name"], artistname, strict): + artist_match = MusicBrainzArtist.from_dict( + replace_hyphens(artist["artist"]) + ) + if not artist_match: + continue + # match album/release + album_match: MusicBrainzReleaseGroup | None = None + for release in item["releases"]: + if compare_strings(release["title"], albumname, strict) or compare_strings( + release["release-group"]["title"], albumname, strict + ): + album_match = MusicBrainzReleaseGroup.from_dict( + replace_hyphens(release["release-group"]) + ) + break + else: + continue + # if we reach this point, we got a match on recording, + # artist and release(group) + recording = MusicBrainzRecording.from_dict(replace_hyphens(item)) + return (artist_match, album_match, recording) + + return None + + async def get_artist_details(self, artist_id: str) -> MusicBrainzArtist: + """Get (full) Artist details by providing a MusicBrainz artist id.""" + endpoint = ( + f"artist/{artist_id}?inc=aliases+annotation+tags+ratings+genres+url-rels+work-rels" + ) + if result := await self.get_data(endpoint): + if "id" not in result: + result["id"] = artist_id + # TODO: Parse all the optional data like relations and such + try: + return MusicBrainzArtist.from_dict(replace_hyphens(result)) + except MissingField as err: + raise InvalidDataError from err + msg = "Invalid MusicBrainz Artist ID provided" + raise InvalidDataError(msg) + + async def get_recording_details(self, recording_id: str) -> MusicBrainzRecording: + """Get Recording details by providing a MusicBrainz Recording Id.""" + if result := await self.get_data(f"recording/{recording_id}?inc=artists+releases"): + if "id" not in result: + result["id"] = recording_id + try: + return MusicBrainzRecording.from_dict(replace_hyphens(result)) + except MissingField as err: + raise InvalidDataError from err + msg = "Invalid MusicBrainz recording ID provided" + raise InvalidDataError(msg) + + async def get_release_details(self, album_id: str) -> MusicBrainzRelease: + """Get Release/Album details by providing a MusicBrainz Album id.""" + endpoint = f"release/{album_id}?inc=artist-credits+aliases+labels" + if result := await self.get_data(endpoint): + if "id" not in result: + result["id"] = album_id + try: + return MusicBrainzRelease.from_dict(replace_hyphens(result)) + except MissingField as err: + raise InvalidDataError from err + msg = "Invalid MusicBrainz Album ID provided" + raise InvalidDataError(msg) + + async def get_releasegroup_details(self, releasegroup_id: str) -> MusicBrainzReleaseGroup: + """Get ReleaseGroup details by providing a MusicBrainz ReleaseGroup id.""" + endpoint = f"release-group/{releasegroup_id}?inc=artists+aliases" + if result := await self.get_data(endpoint): + if "id" not in result: + result["id"] = releasegroup_id + try: + return MusicBrainzReleaseGroup.from_dict(replace_hyphens(result)) + except MissingField as err: + raise InvalidDataError from err + msg = "Invalid MusicBrainz ReleaseGroup ID provided" + raise InvalidDataError(msg) + + async def get_artist_details_by_album( + self, artistname: str, ref_album: Album + ) -> MusicBrainzArtist | None: + """ + Get musicbrainz artist details by providing the artist name and a reference album. + + MusicBrainzArtist object that is returned does not contain the optional data. + """ + result = None + if mb_id := ref_album.get_external_id(ExternalID.MB_RELEASEGROUP): + with suppress(InvalidDataError): + result = await self.get_releasegroup_details(mb_id) + elif mb_id := ref_album.get_external_id(ExternalID.MB_ALBUM): + with suppress(InvalidDataError): + result = await self.get_release_details(mb_id) + else: + return None + if not (result and result.artist_credit): + return None + for strict in (True, False): + for artist_credit in result.artist_credit: + if compare_strings(artist_credit.artist.name, artistname, strict): + return artist_credit.artist + for alias in artist_credit.artist.aliases or []: + if compare_strings(alias.name, artistname, strict): + return artist_credit.artist + return None + + async def get_artist_details_by_track( + self, artistname: str, ref_track: Track + ) -> MusicBrainzArtist | None: + """ + Get musicbrainz artist details by providing the artist name and a reference track. + + MusicBrainzArtist object that is returned does not contain the optional data. + """ + if not ref_track.mbid: + return None + result = None + with suppress(InvalidDataError): + result = await self.get_recording_details(ref_track.mbid) + if not (result and result.artist_credit): + return None + for strict in (True, False): + for artist_credit in result.artist_credit: + if compare_strings(artist_credit.artist.name, artistname, strict): + return artist_credit.artist + for alias in artist_credit.artist.aliases or []: + if compare_strings(alias.name, artistname, strict): + return artist_credit.artist + return None + + async def get_artist_details_by_resource_url( + self, resource_url: str + ) -> MusicBrainzArtist | None: + """ + Get musicbrainz artist details by providing a resource URL (e.g. Spotify share URL). + + MusicBrainzArtist object that is returned does not contain the optional data. + """ + if result := await self.get_data("url", resource=resource_url, inc="artist-rels"): + for relation in result.get("relations", []): + if not (artist := relation.get("artist")): + continue + return MusicBrainzArtist.from_dict(replace_hyphens(artist)) + return None + + @use_cache(86400 * 30) + @throttle_with_retries + async def get_data(self, endpoint: str, **kwargs: dict[str, Any]) -> Any: + """Get data from api.""" + url = f"http://musicbrainz.org/ws/2/{endpoint}" + headers = { + "User-Agent": f"Music Assistant/{self.mass.version} (https://music-assistant.io)" + } + kwargs["fmt"] = "json" # type: ignore[assignment] + async with ( + self.mass.http_session.get(url, headers=headers, params=kwargs) as response, + ): + # handle rate limiter + if response.status == 429: + backoff_time = int(response.headers.get("Retry-After", 0)) + raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + # handle 404 not found + if response.status in (400, 401, 404): + return None + response.raise_for_status() + return await response.json(loads=json_loads) diff --git a/music_assistant/providers/musicbrainz/icon.svg b/music_assistant/providers/musicbrainz/icon.svg new file mode 100644 index 00000000..fde0f687 --- /dev/null +++ b/music_assistant/providers/musicbrainz/icon.svg @@ -0,0 +1 @@ +<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MusicBrainz diff --git a/music_assistant/providers/musicbrainz/icon_dark.svg b/music_assistant/providers/musicbrainz/icon_dark.svg new file mode 100644 index 00000000..249e9ada --- /dev/null +++ b/music_assistant/providers/musicbrainz/icon_dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + +MusicBrainz icon + + + + + + diff --git a/music_assistant/providers/musicbrainz/manifest.json b/music_assistant/providers/musicbrainz/manifest.json new file mode 100644 index 00000000..f3c48835 --- /dev/null +++ b/music_assistant/providers/musicbrainz/manifest.json @@ -0,0 +1,13 @@ +{ + "type": "metadata", + "domain": "musicbrainz", + "name": "MusicBrainz", + "description": "MusicBrainz is an open music encyclopedia that collects music metadata and makes it available to the public. Music Assistant uses MusicBrainz primarily to identify (unique) media items and therefore this provider can not be disabled. However, note that lookups will only be performed if this info is absent locally.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "", + "multi_instance": false, + "builtin": true, + "allow_disable": false, + "icon": "mdi-folder-information" +} diff --git a/music_assistant/providers/opensubsonic/__init__.py b/music_assistant/providers/opensubsonic/__init__.py new file mode 100644 index 00000000..47e05ac5 --- /dev/null +++ b/music_assistant/providers/opensubsonic/__init__.py @@ -0,0 +1,93 @@ +"""Open Subsonic music provider support for MusicAssistant.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ConfigEntryType + +from music_assistant.constants import CONF_PASSWORD, CONF_PATH, CONF_PORT, CONF_USERNAME + +from .sonic_provider import ( + CONF_BASE_URL, + CONF_ENABLE_LEGACY_AUTH, + CONF_ENABLE_PODCASTS, + OpenSonicProvider, +) + +if TYPE_CHECKING: + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return OpenSonicProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, # noqa: ARG001 + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """Return Config entries to setup this provider.""" + return ( + ConfigEntry( + key=CONF_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + description="Your username for this Open Subsonic server", + ), + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password", + required=True, + description="The password associated with the username", + ), + ConfigEntry( + key=CONF_BASE_URL, + type=ConfigEntryType.STRING, + label="Base URL", + required=True, + description="Base URL for the server, e.g. " "https://subsonic.mydomain.tld", + ), + ConfigEntry( + key=CONF_PORT, + type=ConfigEntryType.INTEGER, + label="Port", + required=False, + description="Port Number for the server", + ), + ConfigEntry( + key=CONF_PATH, + type=ConfigEntryType.STRING, + label="Server Path", + required=False, + description="Path to append to base URL for Soubsonic server, this is likely " + "empty unless you are path routing on a proxy", + ), + ConfigEntry( + key=CONF_ENABLE_PODCASTS, + type=ConfigEntryType.BOOLEAN, + label="Enable Podcasts", + required=True, + description="Should the provider query for podcasts as well as music?", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_LEGACY_AUTH, + type=ConfigEntryType.BOOLEAN, + label="Enable legacy auth", + required=True, + description='Enable OpenSubsonic "legacy" auth support', + default_value=False, + ), + ) diff --git a/music_assistant/providers/opensubsonic/icon.svg b/music_assistant/providers/opensubsonic/icon.svg new file mode 100644 index 00000000..429336ab --- /dev/null +++ b/music_assistant/providers/opensubsonic/icon.svg @@ -0,0 +1,11 @@ + + + + Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/music_assistant/providers/opensubsonic/manifest.json b/music_assistant/providers/opensubsonic/manifest.json new file mode 100644 index 00000000..002cb5be --- /dev/null +++ b/music_assistant/providers/opensubsonic/manifest.json @@ -0,0 +1,14 @@ +{ + "type": "music", + "domain": "opensubsonic", + "name": "Open Subsonic Media Server Library", + "description": "Support for Open Subsonic based streaming providers in Music Assistant.", + "codeowners": [ + "@khers" + ], + "requirements": [ + "py-opensonic==5.1.1" + ], + "documentation": "https://music-assistant.io/music-providers/subsonic/", + "multi_instance": true +} diff --git a/music_assistant/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py new file mode 100644 index 00000000..ed1c07a4 --- /dev/null +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -0,0 +1,843 @@ +"""The provider class for Open Subsonic.""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +from libopensonic.connection import Connection as SonicConnection +from libopensonic.errors import ( + AuthError, + CredentialError, + DataNotFoundError, + ParameterError, + SonicError, +) +from music_assistant_models.enums import ( + ContentType, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import LoginFailed, MediaNotFoundError, ProviderPermissionDenied +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + AudioFormat, + ItemMapping, + MediaItemImage, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import ( + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_USERNAME, + UNKNOWN_ARTIST, +) +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable + + from libopensonic.media import Album as SonicAlbum + from libopensonic.media import AlbumInfo as SonicAlbumInfo + from libopensonic.media import Artist as SonicArtist + from libopensonic.media import ArtistInfo as SonicArtistInfo + from libopensonic.media import Playlist as SonicPlaylist + from libopensonic.media import PodcastChannel as SonicPodcastChannel + from libopensonic.media import PodcastEpisode as SonicPodcastEpisode + from libopensonic.media import Song as SonicSong + +CONF_BASE_URL = "baseURL" +CONF_ENABLE_PODCASTS = "enable_podcasts" +CONF_ENABLE_LEGACY_AUTH = "enable_legacy_auth" + +UNKNOWN_ARTIST_ID = "fake_artist_unknown" + +# We need the following prefix because of the way that Navidrome reports artists for individual +# tracks on Various Artists albums, see the note in the _parse_track() method and the handling +# in get_artist() +NAVI_VARIOUS_PREFIX = "MA-NAVIDROME-" + + +class OpenSonicProvider(MusicProvider): + """Provider for Open Subsonic servers.""" + + _conn: SonicConnection = None + _enable_podcasts: bool = True + _seek_support: bool = False + + async def handle_async_init(self) -> None: + """Set up the music provider and test the connection.""" + port = self.config.get_value(CONF_PORT) + if port is None: + port = 443 + path = self.config.get_value(CONF_PATH) + if path is None: + path = "" + self._conn = SonicConnection( + self.config.get_value(CONF_BASE_URL), + username=self.config.get_value(CONF_USERNAME), + password=self.config.get_value(CONF_PASSWORD), + legacyAuth=self.config.get_value(CONF_ENABLE_LEGACY_AUTH), + port=port, + serverPath=path, + appName="Music Assistant", + ) + try: + success = await self._run_async(self._conn.ping) + if not success: + msg = ( + f"Failed to connect to {self.config.get_value(CONF_BASE_URL)}, " + "check your settings." + ) + raise LoginFailed(msg) + except (AuthError, CredentialError) as e: + msg = ( + f"Failed to connect to {self.config.get_value(CONF_BASE_URL)}, check your settings." + ) + raise LoginFailed(msg) from e + self._enable_podcasts = self.config.get_value(CONF_ENABLE_PODCASTS) + try: + ret = await self._run_async(self._conn.getOpenSubsonicExtensions) + extensions = ret["openSubsonicExtensions"] + for entry in extensions: + if entry["name"] == "transcodeOffset": + self._seek_support = True + break + except OSError: + self.logger.info("Server does not support transcodeOffset, seeking in player provider") + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return a list of supported features.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ProviderFeature.PLAYLIST_CREATE, + ) + + @property + def is_streaming_provider(self) -> bool: + """ + Return True if the provider is a streaming provider. + + This literally means that the catalog is not the same as the library contents. + For local based providers (files, plex), the catalog is the same as the library content. + It also means that data is if this provider is NOT a streaming provider, + data cross instances is unique, the catalog and library differs per instance. + + Setting this to True will only query one instance of the provider for search and lookups. + Setting this to False will query all instances of this provider for search and lookups. + """ + return False + + def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: + return ItemMapping( + media_type=media_type, + item_id=key, + provider=self.instance_id, + name=name, + ) + + def _parse_podcast_artist(self, sonic_channel: SonicPodcastChannel) -> Artist: + artist = Artist( + item_id=sonic_channel.id, + name=sonic_channel.title, + provider=self.instance_id, + favorite=bool(sonic_channel.starred), + provider_mappings={ + ProviderMapping( + item_id=sonic_channel.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + if sonic_channel.description is not None: + artist.metadata.description = sonic_channel.description + if sonic_channel.original_image_url: + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=sonic_channel.original_image_url, + provider=self.instance_id, + remotely_accessible=True, + ) + ] + return artist + + def _parse_podcast_album(self, sonic_channel: SonicPodcastChannel) -> Album: + return Album( + item_id=sonic_channel.id, + provider=self.instance_id, + name=sonic_channel.title, + provider_mappings={ + ProviderMapping( + item_id=sonic_channel.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=True, + ) + }, + album_type=AlbumType.PODCAST, + ) + + def _parse_podcast_episode( + self, sonic_episode: SonicPodcastEpisode, sonic_channel: SonicPodcastChannel + ) -> Track: + return Track( + item_id=sonic_episode.id, + provider=self.instance_id, + name=sonic_episode.title, + album=self._parse_podcast_album(sonic_channel=sonic_channel), + artists=[self._parse_podcast_artist(sonic_channel=sonic_channel)], + duration=sonic_episode.duration if sonic_episode.duration is not None else 0, + favorite=bool(sonic_episode.starred), + provider_mappings={ + ProviderMapping( + item_id=sonic_episode.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=True, + ) + }, + ) + + async def _get_podcast_artists(self) -> list[Artist]: + if not self._enable_podcasts: + return [] + + sonic_channels = await self._run_async(self._conn.getPodcasts, incEpisodes=False) + artists = [] + for channel in sonic_channels: + artists.append(self._parse_podcast_artist(channel)) + return artists + + async def _get_podcasts(self) -> list[SonicPodcastChannel]: + if not self._enable_podcasts: + return [] + return await self._run_async(self._conn.getPodcasts, incEpisodes=True) + + def _parse_artist( + self, sonic_artist: SonicArtist, sonic_info: SonicArtistInfo = None + ) -> Artist: + artist = Artist( + item_id=sonic_artist.id, + name=sonic_artist.name, + provider=self.domain, + favorite=bool(sonic_artist.starred), + provider_mappings={ + ProviderMapping( + item_id=sonic_artist.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + + if sonic_artist.cover_id: + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=sonic_artist.cover_id, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + else: + artist.metadata.images = [] + + if sonic_info: + if sonic_info.biography: + artist.metadata.description = sonic_info.biography + if sonic_info.small_url: + artist.metadata.images.append( + MediaItemImage( + type=ImageType.THUMB, + path=sonic_info.small_url, + provider=self.instance_id, + remotely_accessible=True, + ) + ) + return artist + + def _parse_album(self, sonic_album: SonicAlbum, sonic_info: SonicAlbumInfo = None) -> Album: + album_id = sonic_album.id + album = Album( + item_id=album_id, + provider=self.domain, + name=sonic_album.name, + favorite=bool(sonic_album.starred), + provider_mappings={ + ProviderMapping( + item_id=album_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + year=sonic_album.year, + ) + + if sonic_album.cover_id: + album.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=sonic_album.cover_id, + provider=self.instance_id, + remotely_accessible=False, + ), + ] + else: + album.metadata.images = [] + + if sonic_album.artist_id: + album.artists.append( + self._get_item_mapping( + MediaType.ARTIST, + sonic_album.artist_id, + sonic_album.artist if sonic_album.artist else UNKNOWN_ARTIST, + ) + ) + else: + self.logger.info( + f"Unable to find an artist ID for album '{sonic_album.name}' with " + f"ID '{sonic_album.id}'." + ) + album.artists.append( + Artist( + item_id=UNKNOWN_ARTIST_ID, + name=UNKNOWN_ARTIST, + provider=self.instance_id, + provider_mappings={ + ProviderMapping( + item_id=UNKNOWN_ARTIST_ID, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + ) + + if sonic_info: + if sonic_info.small_url: + album.metadata.images.append( + MediaItemImage( + type=ImageType.THUMB, + path=sonic_info.small_url, + remotely_accessible=False, + provider=self.instance_id, + ) + ) + if sonic_info.notes: + album.metadata.description = sonic_info.notes + + return album + + def _parse_track(self, sonic_song: SonicSong) -> Track: + mapping = None + if sonic_song.album_id is not None and sonic_song.album is not None: + mapping = self._get_item_mapping(MediaType.ALBUM, sonic_song.album_id, sonic_song.album) + + track = Track( + item_id=sonic_song.id, + provider=self.instance_id, + name=sonic_song.title, + album=mapping, + duration=sonic_song.duration if sonic_song.duration is not None else 0, + # We are setting disc number to 0 because the standard for what is part of + # a Open Subsonic Song is not yet set and the implementations I have checked + # do not contain this field. We should revisit this when the spec is finished + disc_number=0, + favorite=bool(sonic_song.starred), + provider_mappings={ + ProviderMapping( + item_id=sonic_song.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=True, + audio_format=AudioFormat( + content_type=ContentType.try_parse(sonic_song.content_type) + ), + ) + }, + track_number=getattr(sonic_song, "track", 0), + ) + + # We need to find an artist for this track but various implementations seem to disagree + # about where the artist with the valid ID needs to be found. We will add any artist with + # an ID and only use UNKNOWN if none are found. + + if sonic_song.artist_id: + track.artists.append( + self._get_item_mapping( + MediaType.ARTIST, + sonic_song.artist_id, + sonic_song.artist if sonic_song.artist else UNKNOWN_ARTIST, + ) + ) + + for entry in sonic_song.artists: + if entry.id == sonic_song.artist_id: + continue + if entry.id is not None and entry.name is not None: + track.artists.append(self._get_item_mapping(MediaType.ARTIST, entry.id, entry.name)) + + if not track.artists: + if sonic_song.artist and not sonic_song.artist_id: + # This is how Navidrome handles tracks from albums which are marked + # 'Various Artists'. Unfortunately, we cannot lookup this artist independently + # because it will not have an entry in the artists table so the best we can do it + # add a 'fake' id with the proper artist name and have get_artist() check for this + # id and handle it locally. + artist = Artist( + item_id=f"{NAVI_VARIOUS_PREFIX}{sonic_song.artist}", + provider=self.domain, + name=sonic_song.artist, + provider_mappings={ + ProviderMapping( + item_id=UNKNOWN_ARTIST_ID, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + else: + self.logger.info( + f"Unable to find artist ID for track '{sonic_song.title}' with " + f"ID '{sonic_song.id}'." + ) + artist = Artist( + item_id=UNKNOWN_ARTIST_ID, + name=UNKNOWN_ARTIST, + provider=self.instance_id, + provider_mappings={ + ProviderMapping( + item_id=UNKNOWN_ARTIST_ID, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + + track.artists.append(artist) + return track + + def _parse_playlist(self, sonic_playlist: SonicPlaylist) -> Playlist: + playlist = Playlist( + item_id=sonic_playlist.id, + provider=self.domain, + name=sonic_playlist.name, + is_editable=True, + favorite=bool(sonic_playlist.starred), + provider_mappings={ + ProviderMapping( + item_id=sonic_playlist.id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + if sonic_playlist.cover_id: + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=sonic_playlist.cover_id, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + return playlist + + async def _run_async(self, call: Callable, *args, **kwargs): + return await self.mass.create_task(call, *args, **kwargs) + + async def resolve_image(self, path: str) -> bytes: + """Return the image.""" + + def _get_cover_art() -> bytes: + with self._conn.getCoverArt(path) as art: + return art.content + + return await asyncio.to_thread(_get_cover_art) + + async def search( + self, search_query: str, media_types: list[MediaType], limit: int = 20 + ) -> SearchResults: + """Search the sonic library.""" + artists = limit if MediaType.ARTIST in media_types else 0 + albums = limit if MediaType.ALBUM in media_types else 0 + songs = limit if MediaType.TRACK in media_types else 0 + if not (artists or albums or songs): + return SearchResults() + answer = await self._run_async( + self._conn.search3, + query=search_query, + artistCount=artists, + artistOffset=0, + albumCount=albums, + albumOffset=0, + songCount=songs, + songOffset=0, + musicFolderId=None, + ) + return SearchResults( + artists=[self._parse_artist(entry) for entry in answer["artists"]], + albums=[self._parse_album(entry) for entry in answer["albums"]], + tracks=[self._parse_track(entry) for entry in answer["songs"]], + ) + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Provide a generator for reading all artists.""" + indices = await self._run_async(self._conn.getArtists) + for index in indices: + for artist in index.artists: + yield self._parse_artist(artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """ + Provide a generator for reading all artists. + + Note the pagination, the open subsonic docs say that this method is limited to + returning 500 items per invocation. + """ + offset = 0 + size = 500 + albums = await self._run_async( + self._conn.getAlbumList2, ltype="alphabeticalByArtist", size=size, offset=offset + ) + while albums: + for album in albums: + yield self._parse_album(album) + offset += size + albums = await self._run_async( + self._conn.getAlbumList2, ltype="alphabeticalByArtist", size=size, offset=offset + ) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Provide a generator for library playlists.""" + results = await self._run_async(self._conn.getPlaylists) + for entry in results: + yield self._parse_playlist(entry) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """ + Provide a generator for library tracks. + + Note the lack of item count on this method. + """ + query = "" + offset = 0 + count = 500 + try: + results = await self._run_async( + self._conn.search3, + query=query, + artistCount=0, + albumCount=0, + songOffset=offset, + songCount=count, + ) + except ParameterError: + # Older Navidrome does not accept an empty string and requires the empty quotes + query = '""' + results = await self._run_async( + self._conn.search3, + query=query, + artistCount=0, + albumCount=0, + songOffset=offset, + songCount=count, + ) + while results["songs"]: + for entry in results["songs"]: + yield self._parse_track(entry) + offset += count + results = await self._run_async( + self._conn.search3, + query=query, + artistCount=0, + albumCount=0, + songOffset=offset, + songCount=count, + ) + + async def get_album(self, prov_album_id: str) -> Album: + """Return the requested Album.""" + try: + sonic_album: SonicAlbum = await self._run_async(self._conn.getAlbum, prov_album_id) + sonic_info = await self._run_async(self._conn.getAlbumInfo2, aid=prov_album_id) + except (ParameterError, DataNotFoundError) as e: + if self._enable_podcasts: + # This might actually be a 'faked' album from podcasts, try that before giving up + try: + sonic_channel = await self._run_async( + self._conn.getPodcasts, incEpisodes=False, pid=prov_album_id + ) + return self._parse_podcast_album(sonic_channel=sonic_channel) + except SonicError: + pass + msg = f"Album {prov_album_id} not found" + raise MediaNotFoundError(msg) from e + + return self._parse_album(sonic_album, sonic_info) + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Return a list of tracks on the specified Album.""" + try: + sonic_album: SonicAlbum = await self._run_async(self._conn.getAlbum, prov_album_id) + except (ParameterError, DataNotFoundError) as e: + msg = f"Album {prov_album_id} not found" + raise MediaNotFoundError(msg) from e + tracks = [] + for sonic_song in sonic_album.songs: + tracks.append(self._parse_track(sonic_song)) + return tracks + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Return the requested Artist.""" + if prov_artist_id == UNKNOWN_ARTIST_ID: + return Artist( + item_id=UNKNOWN_ARTIST_ID, + name=UNKNOWN_ARTIST, + provider=self.instance_id, + provider_mappings={ + ProviderMapping( + item_id=UNKNOWN_ARTIST_ID, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + elif prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): + # Special case for handling track artists on various artists album for Navidrome. + return Artist( + item_id=prov_artist_id, + name=prov_artist_id.removeprefix(NAVI_VARIOUS_PREFIX), + provider=self.instance_id, + provider_mappings={ + ProviderMapping( + item_id=prov_artist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + + try: + sonic_artist: SonicArtist = await self._run_async( + self._conn.getArtist, artist_id=prov_artist_id + ) + sonic_info = await self._run_async(self._conn.getArtistInfo2, aid=prov_artist_id) + except (ParameterError, DataNotFoundError) as e: + if self._enable_podcasts: + # This might actually be a 'faked' artist from podcasts, try that before giving up + try: + sonic_channel = await self._run_async( + self._conn.getPodcasts, incEpisodes=False, pid=prov_artist_id + ) + return self._parse_podcast_artist(sonic_channel=sonic_channel[0]) + except SonicError: + pass + msg = f"Artist {prov_artist_id} not found" + raise MediaNotFoundError(msg) from e + return self._parse_artist(sonic_artist, sonic_info) + + async def get_track(self, prov_track_id: str) -> Track: + """Return the specified track.""" + try: + sonic_song: SonicSong = await self._run_async(self._conn.getSong, prov_track_id) + except (ParameterError, DataNotFoundError) as e: + msg = f"Item {prov_track_id} not found" + raise MediaNotFoundError(msg) from e + return self._parse_track(sonic_song) + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Return a list of all Albums by specified Artist.""" + if prov_artist_id == UNKNOWN_ARTIST_ID or prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): + return [] + + try: + sonic_artist: SonicArtist = await self._run_async(self._conn.getArtist, prov_artist_id) + except (ParameterError, DataNotFoundError) as e: + msg = f"Album {prov_artist_id} not found" + raise MediaNotFoundError(msg) from e + albums = [] + for entry in sonic_artist.albums: + albums.append(self._parse_album(entry)) + return albums + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Return the specified Playlist.""" + try: + sonic_playlist: SonicPlaylist = await self._run_async( + self._conn.getPlaylist, prov_playlist_id + ) + except (ParameterError, DataNotFoundError) as e: + msg = f"Playlist {prov_playlist_id} not found" + raise MediaNotFoundError(msg) from e + return self._parse_playlist(sonic_playlist) + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # paging not supported, we always return the whole list at once + return result + try: + sonic_playlist: SonicPlaylist = await self._run_async( + self._conn.getPlaylist, prov_playlist_id + ) + except (ParameterError, DataNotFoundError) as e: + msg = f"Playlist {prov_playlist_id} not found" + raise MediaNotFoundError(msg) from e + + # TODO: figure out if subsonic supports paging here + for index, sonic_song in enumerate(sonic_playlist.songs, 1): + track = self._parse_track(sonic_song) + track.position = index + result.append(track) + return result + + async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: + """Get the top listed tracks for a specified artist.""" + # We have seen top tracks requested for the UNKNOWN_ARTIST ID, protect against that + if prov_artist_id == UNKNOWN_ARTIST_ID or prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): + return [] + + try: + sonic_artist: SonicArtist = await self._run_async(self._conn.getArtist, prov_artist_id) + except DataNotFoundError as e: + msg = f"Artist {prov_artist_id} not found" + raise MediaNotFoundError(msg) from e + songs: list[SonicSong] = await self._run_async(self._conn.getTopSongs, sonic_artist.name) + return [self._parse_track(entry) for entry in songs] + + async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: + """Get tracks similar to selected track.""" + songs: list[SonicSong] = await self._run_async( + self._conn.getSimilarSongs2, iid=prov_track_id, count=limit + ) + return [self._parse_track(entry) for entry in songs] + + async def create_playlist(self, name: str) -> Playlist: + """Create a new empty playlist on the server.""" + playlist: SonicPlaylist = await self._run_async(self._conn.createPlaylist, name=name) + return self._parse_playlist(playlist) + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Append the listed tracks to the selected playlist. + + Note that the configured user must own the playlist to edit this way. + """ + try: + await self._run_async( + self._conn.updatePlaylist, lid=prov_playlist_id, songIdsToAdd=prov_track_ids + ) + except SonicError: + msg = f"Failed to add songs to {prov_playlist_id}, check your permissions." + raise ProviderPermissionDenied(msg) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove selected positions from the playlist.""" + idx_to_remove = [pos - 1 for pos in positions_to_remove] + try: + await self._run_async( + self._conn.updatePlaylist, + lid=prov_playlist_id, + songIndexesToRemove=idx_to_remove, + ) + except SonicError: + msg = f"Failed to remove songs from {prov_playlist_id}, check your permissions." + raise ProviderPermissionDenied(msg) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get the details needed to process a specified track.""" + try: + sonic_song: SonicSong = await self._run_async(self._conn.getSong, item_id) + except (ParameterError, DataNotFoundError) as e: + msg = f"Item {item_id} not found" + raise MediaNotFoundError(msg) from e + + self.mass.create_task(self._report_playback_started(item_id)) + + mime_type = sonic_song.content_type + if mime_type.endswith("mpeg"): + mime_type = sonic_song.suffix + + self.logger.debug( + "Fetching stream details for id %s '%s' with format '%s'", + sonic_song.id, + sonic_song.title, + mime_type, + ) + + return StreamDetails( + item_id=sonic_song.id, + provider=self.instance_id, + can_seek=self._seek_support, + audio_format=AudioFormat(content_type=ContentType.try_parse(mime_type)), + stream_type=StreamType.CUSTOM, + duration=sonic_song.duration if sonic_song.duration is not None else 0, + ) + + async def _report_playback_started(self, item_id: str) -> None: + await self._run_async(self._conn.scrobble, sid=item_id, submission=False) + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + if seconds_streamed >= streamdetails.duration / 2: + await self._run_async(self._conn.scrobble, sid=streamdetails.item_id, submission=True) + + async def get_audio_stream( + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """Provide a generator for the stream data.""" + audio_buffer = asyncio.Queue(1) + + self.logger.debug("Streaming %s", streamdetails.item_id) + + def _streamer() -> None: + with self._conn.stream( + streamdetails.item_id, timeOffset=seek_position, estimateContentLength=True + ) as stream: + for chunk in stream.iter_content(chunk_size=40960): + asyncio.run_coroutine_threadsafe( + audio_buffer.put(chunk), self.mass.loop + ).result() + # send empty chunk when we're done + asyncio.run_coroutine_threadsafe(audio_buffer.put(b"EOF"), self.mass.loop).result() + + # fire up an executor thread to put the audio chunks (threadsafe) on the audio buffer + streamer_task = self.mass.loop.run_in_executor(None, _streamer) + try: + while True: + # keep reading from the audio buffer until there is no more data + chunk = await audio_buffer.get() + if chunk == b"EOF": + break + yield chunk + finally: + if not streamer_task.done(): + streamer_task.cancel() + + self.logger.debug("Done streaming %s", streamdetails.item_id) diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py new file mode 100644 index 00000000..8384da83 --- /dev/null +++ b/music_assistant/providers/player_group/__init__.py @@ -0,0 +1,853 @@ +""" +Sync Group Player provider. + +This is more like a "virtual" player provider, +allowing the user to create 'presets' of players to sync together (of the same type). +""" + +from __future__ import annotations + +from collections.abc import Callable +from contextlib import suppress +from time import time +from typing import TYPE_CHECKING, Final, cast + +import shortuuid +from aiohttp import web +from music_assistant_models.config_entries import ( + BASE_PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_PLAYER_ICON_GROUP, + ConfigEntry, + ConfigValueOption, + ConfigValueType, + PlayerConfig, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + EventType, + MediaType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderFeature, +) +from music_assistant_models.errors import ( + PlayerUnavailableError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.constants import ( + CONF_CROSSFADE, + CONF_CROSSFADE_DURATION, + CONF_ENABLE_ICY_METADATA, + CONF_ENFORCE_MP3, + CONF_FLOW_MODE, + CONF_GROUP_MEMBERS, + CONF_HTTP_PROFILE, + CONF_SAMPLE_RATES, +) +from music_assistant.controllers.streams import DEFAULT_STREAM_HEADERS +from music_assistant.helpers.ffmpeg import get_ffmpeg_stream +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider + +from .ugp_stream import UGP_FORMAT, UGPStream + +if TYPE_CHECKING: + from collections.abc import Iterable + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.event import MassEvent + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +# ruff: noqa: ARG002 + +UNIVERSAL_PREFIX: Final[str] = "ugp_" +SYNCGROUP_PREFIX: Final[str] = "syncgroup_" +GROUP_TYPE_UNIVERSAL: Final[str] = "universal" +CONF_GROUP_TYPE: Final[str] = "group_type" +CONF_ENTRY_GROUP_TYPE = ConfigEntry( + key=CONF_GROUP_TYPE, + type=ConfigEntryType.STRING, + label="Group type", + default_value="universal", + hidden=True, + required=True, +) +CONF_ENTRY_GROUP_MEMBERS = ConfigEntry( + key=CONF_GROUP_MEMBERS, + type=ConfigEntryType.STRING, + label="Group members", + default_value=[], + description="Select all players you want to be part of this group", + multi_value=True, + required=True, +) +CONF_ENTRY_SAMPLE_RATES_UGP = create_sample_rates_config_entry(44100, 16, 44100, 16, True) +CONFIG_ENTRY_UGP_NOTE = ConfigEntry( + key="ugp_note", + type=ConfigEntryType.LABEL, + label="Please note that although the Universal Group " + "allows you to group any player, it will not enable audio sync " + "between players of different ecosystems. It is advised to always use native " + "player groups or sync groups when available for your player type(s) and use " + "the Universal Group only to group players of different ecosystems.", + required=False, +) +CONFIG_ENTRY_DYNAMIC_MEMBERS = ConfigEntry( + key="dynamic_members", + type=ConfigEntryType.BOOLEAN, + label="Enable dynamic members (experimental)", + description="Allow members to (temporary) join/leave the group dynamically, " + "so the group more or less behaves the same like manually syncing players together, " + "with the main difference being that the groupplayer will hold the queue. \n\n" + "NOTE: This is an experimental feature which we are testing out. " + "You may run into some unexpected behavior!", + default_value=False, + required=False, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return PlayerGroupProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, # noqa: ARG001 + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # nothing to configure (for now) + return () + + +class PlayerGroupProvider(PlayerProvider): + """Base/builtin provider for creating (permanent) player groups.""" + + def __init__( + self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig + ) -> None: + """Initialize MusicProvider.""" + super().__init__(mass, manifest, config) + self.ugp_streams: dict[str, UGPStream] = {} + self._on_unload: list[Callable[[], None]] = [ + self.mass.register_api_command("player_group/create", self.create_group), + ] + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.REMOVE_PLAYER,) + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + await super().loaded_in_mass() + # temp: migrate old config entries + # remove this after MA 2.4 release + for player_config in await self.mass.config.get_player_configs(): + if player_config.provider == self.instance_id: + # already migrated + continue + # migrate old syncgroup players to this provider + if player_config.player_id.startswith(SYNCGROUP_PREFIX): + self.mass.config.set_raw_player_config_value( + player_config.player_id, CONF_GROUP_TYPE, player_config.provider + ) + player_config.provider = self.instance_id + self.mass.config.set_raw_player_config_value( + player_config.player_id, "provider", self.instance_id + ) + # migrate old UGP players to this provider + elif player_config.player_id.startswith(UNIVERSAL_PREFIX): + self.mass.config.set_raw_player_config_value( + player_config.player_id, CONF_GROUP_TYPE, "universal" + ) + player_config.provider = self.instance_id + self.mass.config.set_raw_player_config_value( + player_config.player_id, "provider", self.instance_id + ) + + await self._register_all_players() + # listen for player added events so we can catch late joiners + # (because a group depends on its childs to be available) + self._on_unload.append( + self.mass.subscribe(self._on_mass_player_added_event, EventType.PLAYER_ADDED) + ) + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + for unload_cb in self._on_unload: + unload_cb() + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + # default entries for player groups + base_entries = ( + *BASE_PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_PLAYER_ICON_GROUP, + CONF_ENTRY_GROUP_TYPE, + CONF_ENTRY_GROUP_MEMBERS, + ) + # group type is static and can not be changed. we just grab the existing, stored value + group_type: str = self.mass.config.get_raw_player_config_value( + player_id, CONF_GROUP_TYPE, GROUP_TYPE_UNIVERSAL + ) + # handle config entries for universal group players + if group_type == GROUP_TYPE_UNIVERSAL: + group_members = CONF_ENTRY_GROUP_MEMBERS + group_members.options = tuple( + ConfigValueOption(x.display_name, x.player_id) + for x in self.mass.players.all(True, False) + if not x.player_id.startswith(UNIVERSAL_PREFIX) + ) + return ( + *base_entries, + group_members, + CONFIG_ENTRY_UGP_NOTE, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_SAMPLE_RATES_UGP, + CONF_ENTRY_FLOW_MODE_ENFORCED, + ) + # handle config entries for syncgroup players + group_members = CONF_ENTRY_GROUP_MEMBERS + if player_prov := self.mass.get_provider(group_type): + group_members.options = tuple( + ConfigValueOption(x.display_name, x.player_id) for x in player_prov.players + ) + + # grab additional details from one of the provider's players + if not (player_provider := self.mass.get_provider(group_type)): + return base_entries # guard + if TYPE_CHECKING: + player_provider = cast(PlayerProvider, player_provider) + assert player_provider.lookup_key != self.lookup_key + if not (child_player := next((x for x in player_provider.players), None)): + return base_entries # guard + + # combine base group entries with (base) player entries for this player type + allowed_conf_entries = ( + CONF_HTTP_PROFILE, + CONF_ENABLE_ICY_METADATA, + CONF_CROSSFADE, + CONF_CROSSFADE_DURATION, + CONF_ENFORCE_MP3, + CONF_FLOW_MODE, + CONF_SAMPLE_RATES, + ) + child_config_entries = await player_provider.get_player_config_entries( + child_player.player_id + ) + return ( + *base_entries, + group_members, + CONFIG_ENTRY_DYNAMIC_MEMBERS, + *(entry for entry in child_config_entries if entry.key in allowed_conf_entries), + ) + + async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: + """Call (by config manager) when the configuration of a player changes.""" + if f"values/{CONF_GROUP_MEMBERS}" in changed_keys: + members = config.get_value(CONF_GROUP_MEMBERS) + # ensure we filter invalid members + members = self._filter_members(config.get_value(CONF_GROUP_TYPE), members) + if group_player := self.mass.players.get(config.player_id): + group_player.group_childs = members + if group_player.powered: + # power on group player (which will also resync) if needed + await self.cmd_power(group_player.player_id, True) + if f"values/{CONFIG_ENTRY_DYNAMIC_MEMBERS.key}" in changed_keys: + # dynamic members feature changed + if group_player := self.mass.players.get(config.player_id): + if PlayerFeature.SYNC in group_player.supported_features: + group_player.supported_features = tuple( + x for x in group_player.supported_features if x != PlayerFeature.SYNC + ) + else: + group_player.supported_features = ( + *group_player.supported_features, + PlayerFeature.SYNC, + ) + await super().on_player_config_change(config, changed_keys) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + group_player = self.mass.players.get(player_id) + # syncgroup: forward command to sync leader + if player_id.startswith(SYNCGROUP_PREFIX): + if sync_leader := self._get_sync_leader(group_player): + if player_provider := self.mass.get_provider(sync_leader.provider): + await player_provider.cmd_stop(sync_leader.player_id) + return + # ugp: forward command to all members + async with TaskManager(self.mass) as tg: + for member in self.mass.players.iter_group_members(group_player, active_only=True): + if player_provider := self.mass.get_provider(member.provider): + tg.create_task(player_provider.cmd_stop(member.player_id)) + # abort the stream session + if (stream := self.ugp_streams.pop(player_id, None)) and not stream.done: + await stream.stop() + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + group_player = self.mass.players.get(player_id) + if not player_id.startswith(SYNCGROUP_PREFIX): + # this shouldn't happen, but just in case + raise UnsupportedFeaturedException + # forward command to sync leader + if sync_leader := self._get_sync_leader(group_player): + if player_provider := self.mass.get_provider(sync_leader.provider): + await player_provider.cmd_play(sync_leader.player_id) + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + group_player = self.mass.players.get(player_id) + if not player_id.startswith(SYNCGROUP_PREFIX): + # this shouldn't happen, but just in case + raise UnsupportedFeaturedException + # forward command to sync leader + if sync_leader := self._get_sync_leader(group_player): + if player_provider := self.mass.get_provider(sync_leader.provider): + await player_provider.cmd_pause(sync_leader.player_id) + + async def cmd_power(self, player_id: str, powered: bool) -> None: + """Handle POWER command to group player.""" + group_player = self.mass.players.get(player_id, raise_unavailable=True) + if TYPE_CHECKING: + group_player = cast(Player, group_player) + + # always stop at power off + if not powered and group_player.state in (PlayerState.PLAYING, PlayerState.PAUSED): + await self.cmd_stop(group_player.player_id) + + # always (re)fetch the configured group members at power on + if not group_player.powered: + group_member_ids = self.mass.config.get_raw_player_config_value( + player_id, CONF_GROUP_MEMBERS, [] + ) + group_player.group_childs = { + x + for x in group_member_ids + if (child_player := self.mass.players.get(x)) + and child_player.available + and child_player.enabled + } + + if powered: + # handle TURN_ON of the group player by turning on all members + for member in self.mass.players.iter_group_members( + group_player, only_powered=False, active_only=False + ): + player_provider = self.mass.get_provider(member.provider) + assert player_provider # for typing + if ( + member.state in (PlayerState.PLAYING, PlayerState.PAUSED) + and member.active_source != group_player.active_source + ): + # stop playing existing content on member if we start the group player + await player_provider.cmd_stop(member.player_id) + if not member.powered: + member.active_group = None # needed to prevent race conditions + await self.mass.players.cmd_power(member.player_id, True) + # set active source to group player if the group (is going to be) powered + member.active_group = group_player.player_id + member.active_source = group_player.active_source + else: + # handle TURN_OFF of the group player by turning off all members + # optimistically set the group state to prevent race conditions + # with the unsync command + group_player.powered = False + for member in self.mass.players.iter_group_members( + group_player, only_powered=True, active_only=True + ): + # reset active group on player when the group is turned off + member.active_group = None + member.active_source = None + # handle TURN_OFF of the group player by turning off all members + if member.powered: + await self.mass.players.cmd_power(member.player_id, False) + + if powered and player_id.startswith(SYNCGROUP_PREFIX): + await self._sync_syncgroup(group_player) + # optimistically set the group state + group_player.powered = powered + self.mass.players.update(group_player.player_id) + if not powered: + # reset the group members when powered off + group_player.group_childs = set( + self.mass.config.get_raw_player_config_value(player_id, CONF_GROUP_MEMBERS, []) + ) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + # group volume is already handled in the player manager + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + group_player = self.mass.players.get(player_id) + # power on (which will also resync) if needed + await self.cmd_power(player_id, True) + + # handle play_media for sync group + if player_id.startswith(SYNCGROUP_PREFIX): + # simply forward the command to the sync leader + sync_leader = self._select_sync_leader(group_player) + assert sync_leader # for typing + player_provider = self.mass.get_provider(sync_leader.provider) + assert player_provider # for typing + await player_provider.play_media( + sync_leader.player_id, + media=media, + ) + return + + # handle play_media for UGP group + if (existing := self.ugp_streams.pop(player_id, None)) and not existing.done: + # stop any existing stream first + await existing.stop() + + # select audio source + if media.media_type == MediaType.ANNOUNCEMENT: + # special case: stream announcement + audio_source = self.mass.streams.get_announcement_stream( + media.custom_data["url"], + output_format=UGP_FORMAT, + use_pre_announce=media.custom_data["use_pre_announce"], + ) + elif media.queue_id and media.queue_item_id: + # regular queue stream request + audio_source = self.mass.streams.get_flow_stream( + queue=self.mass.player_queues.get(media.queue_id), + start_queue_item=self.mass.player_queues.get_item( + media.queue_id, media.queue_item_id + ), + pcm_format=UGP_FORMAT, + ) + else: + # assume url or some other direct path + # NOTE: this will fail if its an uri not playable by ffmpeg + audio_source = get_ffmpeg_stream( + audio_input=media.uri, + input_format=AudioFormat(ContentType.try_parse(media.uri)), + output_format=UGP_FORMAT, + ) + + # start the stream task + self.ugp_streams[player_id] = UGPStream(audio_source=audio_source, audio_format=UGP_FORMAT) + base_url = f"{self.mass.streams.base_url}/ugp/{player_id}.mp3" + + # set the state optimistically + group_player.current_media = media + group_player.elapsed_time = 0 + group_player.elapsed_time_last_updated = time() - 1 + group_player.state = PlayerState.PLAYING + self.mass.players.update(player_id) + + # forward to downstream play_media commands + async with TaskManager(self.mass) as tg: + for member in self.mass.players.iter_group_members( + group_player, only_powered=True, active_only=True + ): + player_provider = self.mass.get_provider(member.provider) + assert player_provider # for typing + tg.create_task( + player_provider.play_media( + member.player_id, + media=PlayerMedia( + uri=f"{base_url}?player_id={member.player_id}", + media_type=MediaType.FLOW_STREAM, + title=group_player.display_name, + queue_id=group_player.player_id, + ), + ) + ) + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of a next media item on the player.""" + group_player = self.mass.players.get(player_id, True) + if not player_id.startswith(SYNCGROUP_PREFIX): + # this shouldn't happen, but just in case + raise UnsupportedFeaturedException("Command is not supported for UGP players") + if sync_leader := self._get_sync_leader(group_player): + await self.mass.players.enqueue_next_media( + sync_leader.player_id, + media=media, + ) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates. + + This is called by the Player Manager; + if 'needs_poll' is set to True in the player object. + """ + if group_player := self.mass.players.get(player_id): + self._update_attributes(group_player) + + async def create_group(self, group_type: str, name: str, members: list[str]) -> Player: + """Create new Group Player.""" + # perform basic checks + if group_type == GROUP_TYPE_UNIVERSAL: + prefix = UNIVERSAL_PREFIX + else: + prefix = SYNCGROUP_PREFIX + if (player_prov := self.mass.get_provider(group_type)) is None: + msg = f"Provider {group_type} is not available!" + raise ProviderUnavailableError(msg) + if ProviderFeature.SYNC_PLAYERS not in player_prov.supported_features: + msg = f"Provider {player_prov.name} does not support creating groups" + raise UnsupportedFeaturedException(msg) + + new_group_id = f"{prefix}{shortuuid.random(8).lower()}" + # cleanup list, just in case the frontend sends some garbage + members = self._filter_members(group_type, members) + # create default config with the user chosen name + self.mass.config.create_default_player_config( + new_group_id, + self.instance_id, + name=name, + enabled=True, + values={CONF_GROUP_MEMBERS: members, CONF_GROUP_TYPE: group_type}, + ) + return await self._register_group_player( + group_player_id=new_group_id, group_type=group_type, name=name, members=members + ) + + async def remove_player(self, player_id: str) -> None: + """Remove a group player.""" + if not (group_player := self.mass.players.get(player_id)): + return + if group_player.powered: + # edge case: the group player is powered and being removed + # make sure to turn it off first (which will also unsync a syncgroup) + await self.cmd_power(player_id, False) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the sync leader. + """ + group_player = self.mass.players.get(target_player, raise_unavailable=True) + if TYPE_CHECKING: + group_player = cast(Player, group_player) + dynamic_members_enabled = self.mass.config.get_raw_player_config_value( + group_player.player_id, + CONFIG_ENTRY_DYNAMIC_MEMBERS.key, + CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, + ) + group_type = self.mass.config.get_raw_player_config_value( + group_player.player_id, CONF_ENTRY_GROUP_TYPE.key, CONF_ENTRY_GROUP_TYPE.default_value + ) + if not dynamic_members_enabled: + raise UnsupportedFeaturedException( + f"Adjusting group members is not allowed for group {group_player.display_name}" + ) + new_members = self._filter_members(group_type, [*group_player.group_childs, player_id]) + group_player.group_childs = new_members + if group_player.powered: + # power on group player (which will also resync) if needed + await self.cmd_power(target_player, True) + + async def cmd_unsync_member(self, player_id: str, target_player: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player(id) from the given (master) player/sync group. + + - player_id: player_id of the (child) player to unsync from the group. + - target_player: player_id of the group player. + """ + group_player = self.mass.players.get(target_player, raise_unavailable=True) + child_player = self.mass.players.get(player_id, raise_unavailable=True) + if TYPE_CHECKING: + group_player = cast(Player, group_player) + child_player = cast(Player, child_player) + dynamic_members_enabled = self.mass.config.get_raw_player_config_value( + group_player.player_id, + CONFIG_ENTRY_DYNAMIC_MEMBERS.key, + CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, + ) + if group_player.powered and not dynamic_members_enabled: + raise UnsupportedFeaturedException( + f"Adjusting group members is not allowed for group {group_player.display_name}" + ) + is_sync_leader = len(child_player.group_childs) > 0 + was_playing = child_player.state == PlayerState.PLAYING + # forward command to the player provider + if player_provider := self.mass.players.get_player_provider(child_player.player_id): + await player_provider.cmd_unsync(child_player.player_id) + child_player.active_group = None + child_player.active_source = None + group_player.group_childs = {x for x in group_player.group_childs if x != player_id} + if is_sync_leader and was_playing: + # unsyncing the sync leader will stop the group so we need to resume + self.mass.call_later(2, self.mass.players.cmd_play, group_player.player_id) + elif group_player.powered: + # power on group player (which will also resync) if needed + await self.cmd_power(group_player.player_id, True) + + async def _register_all_players(self) -> None: + """Register all (virtual/fake) group players in the Player controller.""" + player_configs = await self.mass.config.get_player_configs( + self.instance_id, include_values=True + ) + for player_config in player_configs: + if self.mass.players.get(player_config.player_id): + continue # already registered + members = player_config.get_value(CONF_GROUP_MEMBERS) + group_type = player_config.get_value(CONF_GROUP_TYPE) + with suppress(PlayerUnavailableError): + await self._register_group_player( + player_config.player_id, + group_type, + player_config.name or player_config.default_name, + members, + ) + + async def _register_group_player( + self, group_player_id: str, group_type: str, name: str, members: Iterable[str] + ) -> Player: + """Register a syncgroup player.""" + player_features = {PlayerFeature.POWER, PlayerFeature.VOLUME_SET} + + if not (self.mass.players.get(x) for x in members): + raise PlayerUnavailableError("One or more members are not available!") + + if group_type == GROUP_TYPE_UNIVERSAL: + model_name = "Universal Group" + manufacturer = self.name + # register dynamic route for the ugp stream + route_path = f"/ugp/{group_player_id}.mp3" + self._on_unload.append( + self.mass.streams.register_dynamic_route(route_path, self._serve_ugp_stream) + ) + elif player_provider := self.mass.get_provider(group_type): + # grab additional details from one of the provider's players + if TYPE_CHECKING: + player_provider = cast(PlayerProvider, player_provider) + model_name = "Sync Group" + manufacturer = self.mass.get_provider(group_type).name + for feature in (PlayerFeature.PAUSE, PlayerFeature.VOLUME_MUTE, PlayerFeature.ENQUEUE): + if all(feature in x.supported_features for x in player_provider.players): + player_features.add(feature) + else: + raise PlayerUnavailableError(f"Provider for syncgroup {group_type} is not available!") + + if self.mass.config.get_raw_player_config_value( + group_player_id, + CONFIG_ENTRY_DYNAMIC_MEMBERS.key, + CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, + ): + player_features.add(PlayerFeature.SYNC) + + player = Player( + player_id=group_player_id, + provider=self.instance_id, + type=PlayerType.GROUP, + name=name, + available=True, + powered=False, + device_info=DeviceInfo(model=model_name, manufacturer=manufacturer), + supported_features=tuple(player_features), + group_childs=set(members), + active_source=group_player_id, + needs_poll=True, + poll_interval=30, + ) + + await self.mass.players.register_or_update(player) + self._update_attributes(player) + return player + + def _get_sync_leader(self, group_player: Player) -> Player | None: + """Get the active sync leader player for the syncgroup.""" + if group_player.synced_to: + # should not happen but just in case... + return self.mass.players.get(group_player.synced_to) + if len(group_player.group_childs) == 1: + # Return the (first/only) player + # this is to handle the edge case where players are not + # yet synced or there simply is just one player + for child_player in self.mass.players.iter_group_members( + group_player, only_powered=False, only_playing=False, active_only=False + ): + if not child_player.synced_to: + return child_player + # Return the (first/only) player that has group childs + for child_player in self.mass.players.iter_group_members( + group_player, only_powered=False, only_playing=False, active_only=False + ): + if child_player.group_childs: + return child_player + return None + + def _select_sync_leader(self, group_player: Player) -> Player | None: + """Select the active sync leader player for a syncgroup.""" + if sync_leader := self._get_sync_leader(group_player): + return sync_leader + # select new sync leader: return the first active player + for child_player in self.mass.players.iter_group_members(group_player, active_only=True): + if child_player.active_group not in (None, group_player.player_id): + continue + if ( + child_player.active_source + and child_player.active_source != group_player.active_source + ): + continue + return child_player + # fallback select new sync leader: simply return the first (available) player + for child_player in self.mass.players.iter_group_members( + group_player, only_powered=False, only_playing=False, active_only=False + ): + return child_player + # this really should not be possible + raise RuntimeError("No players available to form syncgroup") + + async def _sync_syncgroup(self, group_player: Player) -> None: + """Sync all (possible) players of a syncgroup.""" + sync_leader = self._select_sync_leader(group_player) + members_to_sync: list[str] = [] + for member in self.mass.players.iter_group_members(group_player, active_only=False): + if member.synced_to and member.synced_to != sync_leader.player_id: + # unsync first + await self.mass.players.cmd_unsync(member.player_id) + if sync_leader.player_id == member.player_id: + # skip sync leader + continue + if ( + member.synced_to == sync_leader.player_id + and member.player_id in sync_leader.group_childs + ): + # already synced + continue + members_to_sync.append(member.player_id) + if members_to_sync: + await self.mass.players.cmd_sync_many(sync_leader.player_id, members_to_sync) + + async def _on_mass_player_added_event(self, event: MassEvent) -> None: + """Handle player added event from player controller.""" + await self._register_all_players() + + def _update_attributes(self, player: Player) -> None: + """Update attributes of a player.""" + for child_player in self.mass.players.iter_group_members(player, active_only=True): + # just grab the first active player + if child_player.synced_to: + continue + player.state = child_player.state + if child_player.current_media: + player.current_media = child_player.current_media + player.elapsed_time = child_player.elapsed_time + player.elapsed_time_last_updated = child_player.elapsed_time_last_updated + break + else: + player.state = PlayerState.IDLE + player.active_source = player.player_id + self.mass.players.update(player.player_id) + + async def _serve_ugp_stream(self, request: web.Request) -> web.Response: + """Serve the UGP (multi-client) flow stream audio to a player.""" + ugp_player_id = request.path.rsplit(".")[0].rsplit("/")[-1] + child_player_id = request.query.get("player_id") # optional! + + if not (ugp_player := self.mass.players.get(ugp_player_id)): + raise web.HTTPNotFound(reason=f"Unknown UGP player: {ugp_player_id}") + + if not (stream := self.ugp_streams.get(ugp_player_id, None)) or stream.done: + raise web.HTTPNotFound(body=f"There is no active UGP stream for {ugp_player_id}!") + + http_profile: str = await self.mass.config.get_player_config_value( + child_player_id, CONF_HTTP_PROFILE + ) + headers = { + **DEFAULT_STREAM_HEADERS, + "Content-Type": "audio/mp3", + "Accept-Ranges": "none", + "Cache-Control": "no-cache", + "Connection": "close", + } + + resp = web.StreamResponse(status=200, reason="OK", headers=headers) + if http_profile == "forced_content_length": + resp.content_length = 4294967296 + elif http_profile == "chunked": + resp.enable_chunked_encoding() + + await resp.prepare(request) + + # return early if this is not a GET request + if request.method != "GET": + return resp + + # all checks passed, start streaming! + self.logger.debug( + "Start serving UGP flow audio stream for UGP-player %s to %s", + ugp_player.display_name, + child_player_id or request.remote, + ) + async for chunk in stream.subscribe(): + try: + await resp.write(chunk) + except (ConnectionError, ConnectionResetError): + break + + return resp + + def _filter_members(self, group_type: str, members: list[str]) -> list[str]: + """Filter out members that are not valid players.""" + if group_type != GROUP_TYPE_UNIVERSAL: + player_provider = self.mass.get_provider(group_type) + return [ + x + for x in members + if (player := self.mass.players.get(x)) + and player.provider in (player_provider.instance_id, self.instance_id) + ] + # cleanup members - filter out impossible choices + syncgroup_childs: list[str] = [] + for member in members: + if not member.startswith(SYNCGROUP_PREFIX): + continue + if syncgroup := self.mass.players.get(member): + syncgroup_childs.extend(syncgroup.group_childs) + # we filter out other UGP players and syncgroup childs + # if their parent is already in the list + return [ + x + for x in members + if self.mass.players.get(x) + and x not in syncgroup_childs + and not x.startswith(UNIVERSAL_PREFIX) + ] diff --git a/music_assistant/providers/player_group/manifest.json b/music_assistant/providers/player_group/manifest.json new file mode 100644 index 00000000..9c2da78e --- /dev/null +++ b/music_assistant/providers/player_group/manifest.json @@ -0,0 +1,13 @@ +{ + "type": "player", + "domain": "player_group", + "name": "Playergroup", + "description": "Create (permanent) groups of your favorite players. \nSupports both syncgroups (to group speakers of the same ecocystem to play in sync) and universal groups to group speakers of different ecosystems to play the same audio (but not in sync).", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/faq/groups/", + "multi_instance": false, + "builtin": true, + "allow_disable": false, + "icon": "speaker-multiple" +} diff --git a/music_assistant/providers/player_group/ugp_stream.py b/music_assistant/providers/player_group/ugp_stream.py new file mode 100644 index 00000000..f5d8b8a1 --- /dev/null +++ b/music_assistant/providers/player_group/ugp_stream.py @@ -0,0 +1,96 @@ +""" +Implementation of a Stream for the Universal Group Player. + +Basically this is like a fake radio radio stream (MP3) format with multiple subscribers. +The MP3 format is chosen because it is widely supported. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator, Awaitable, Callable + +from music_assistant_models.enums import ContentType +from music_assistant_models.media_items import AudioFormat + +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.util import empty_queue + +# ruff: noqa: ARG002 + +UGP_FORMAT = AudioFormat( + content_type=ContentType.PCM_F32LE, + sample_rate=48000, + bit_depth=32, +) + + +class UGPStream: + """ + Implementation of a Stream for the Universal Group Player. + + Basically this is like a fake radio radio stream (MP3) format with multiple subscribers. + The MP3 format is chosen because it is widely supported. + """ + + def __init__( + self, + audio_source: AsyncGenerator[bytes, None], + audio_format: AudioFormat, + ) -> None: + """Initialize UGP Stream.""" + self.audio_source = audio_source + self.input_format = audio_format + self.output_format = AudioFormat(content_type=ContentType.MP3) + self.subscribers: list[Callable[[bytes], Awaitable]] = [] + self._task: asyncio.Task | None = None + self._done: asyncio.Event = asyncio.Event() + + @property + def done(self) -> bool: + """Return if this stream is already done.""" + return self._done.is_set() and self._task and self._task.done() + + async def stop(self) -> None: + """Stop/cancel the stream.""" + if self._done.is_set(): + return + if self._task and not self._task.done(): + self._task.cancel() + self._done.set() + + async def subscribe(self) -> AsyncGenerator[bytes, None]: + """Subscribe to the raw/unaltered audio stream.""" + # start the runner as soon as the (first) client connects + if not self._task: + self._task = asyncio.create_task(self._runner()) + queue = asyncio.Queue(10) + try: + self.subscribers.append(queue.put) + while True: + chunk = await queue.get() + if not chunk: + break + yield chunk + finally: + self.subscribers.remove(queue.put) + empty_queue(queue) + del queue + + async def _runner(self) -> None: + """Run the stream for the given audio source.""" + await asyncio.sleep(0.25) # small delay to allow subscribers to connect + async for chunk in get_ffmpeg_stream( + audio_input=self.audio_source, + input_format=self.input_format, + output_format=self.output_format, + # we don't allow the player to buffer too much ahead so we use readrate limiting + extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], + ): + await asyncio.gather( + *[sub(chunk) for sub in self.subscribers], + return_exceptions=True, + ) + # empty chunk when done + await asyncio.gather(*[sub(b"") for sub in self.subscribers], return_exceptions=True) + self._done.set() diff --git a/music_assistant/providers/plex/__init__.py b/music_assistant/providers/plex/__init__.py new file mode 100644 index 00000000..5bf783d9 --- /dev/null +++ b/music_assistant/providers/plex/__init__.py @@ -0,0 +1,967 @@ +"""Plex musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import logging +from asyncio import Task, TaskGroup +from collections.abc import Awaitable +from contextlib import suppress +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast + +import plexapi.exceptions +import requests +from music_assistant_models.config_entries import ( + ConfigEntry, + ConfigValueOption, + ConfigValueType, + ProviderConfig, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import ( + InvalidDataError, + LoginFailed, + MediaNotFoundError, + SetupFailedError, +) +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + ItemMapping, + MediaItem, + MediaItemChapter, + MediaItemImage, + Playlist, + ProviderMapping, + SearchResults, + Track, + UniqueList, +) +from music_assistant_models.streamdetails import StreamDetails +from plexapi.audio import Album as PlexAlbum +from plexapi.audio import Artist as PlexArtist +from plexapi.audio import Playlist as PlexPlaylist +from plexapi.audio import Track as PlexTrack +from plexapi.base import PlexObject +from plexapi.myplex import MyPlexAccount, MyPlexPinLogin +from plexapi.server import PlexServer + +from music_assistant.constants import UNKNOWN_ARTIST +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.tags import parse_tags +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.music_provider import MusicProvider +from music_assistant.providers.plex.helpers import discover_local_servers, get_libraries + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable, Coroutine + + from music_assistant_models.provider import ProviderManifest + from plexapi.library import MusicSection as PlexMusicSection + from plexapi.media import AudioStream as PlexAudioStream + from plexapi.media import Media as PlexMedia + from plexapi.media import MediaPart as PlexMediaPart + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +CONF_ACTION_AUTH_MYPLEX = "auth_myplex" +CONF_ACTION_AUTH_LOCAL = "auth_local" +CONF_ACTION_CLEAR_AUTH = "auth" +CONF_ACTION_LIBRARY = "library" +CONF_ACTION_GDM = "gdm" + +CONF_AUTH_TOKEN = "token" +CONF_LIBRARY_ID = "library_id" +CONF_LOCAL_SERVER_IP = "local_server_ip" +CONF_LOCAL_SERVER_PORT = "local_server_port" +CONF_LOCAL_SERVER_SSL = "local_server_ssl" +CONF_LOCAL_SERVER_VERIFY_CERT = "local_server_verify_cert" + +FAKE_ARTIST_PREFIX = "_fake://" + +AUTH_TOKEN_UNAUTH = "local_auth" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + if not config.get_value(CONF_AUTH_TOKEN): + msg = "Invalid login credentials" + raise LoginFailed(msg) + + return PlexProvider(mass, manifest, config) + + +async def get_config_entries( # noqa: PLR0915 + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # handle action GDM discovery + if action == CONF_ACTION_GDM: + server_details = await discover_local_servers() + if server_details and server_details[0] and server_details[1]: + assert values + values[CONF_LOCAL_SERVER_IP] = server_details[0] + values[CONF_LOCAL_SERVER_PORT] = server_details[1] + values[CONF_LOCAL_SERVER_SSL] = False + values[CONF_LOCAL_SERVER_VERIFY_CERT] = False + else: + assert values + values[CONF_LOCAL_SERVER_IP] = "Discovery failed, please add IP manually" + values[CONF_LOCAL_SERVER_PORT] = 32400 + values[CONF_LOCAL_SERVER_SSL] = False + values[CONF_LOCAL_SERVER_VERIFY_CERT] = True + + # handle action clear authentication + if action == CONF_ACTION_CLEAR_AUTH: + assert values + values[CONF_AUTH_TOKEN] = None + values[CONF_LOCAL_SERVER_IP] = None + values[CONF_LOCAL_SERVER_PORT] = 32400 + values[CONF_LOCAL_SERVER_SSL] = False + values[CONF_LOCAL_SERVER_VERIFY_CERT] = True + + # handle action MyPlex auth + if action == CONF_ACTION_AUTH_MYPLEX: + assert values + values[CONF_AUTH_TOKEN] = None + async with AuthenticationHelper(mass, str(values["session_id"])) as auth_helper: + plex_auth = MyPlexPinLogin(headers={"X-Plex-Product": "Music Assistant"}, oauth=True) + auth_url = plex_auth.oauthUrl(auth_helper.callback_url) + await auth_helper.authenticate(auth_url) + if not plex_auth.checkLogin(): + msg = "Authentication to MyPlex failed" + raise LoginFailed(msg) + # set the retrieved token on the values object to pass along + values[CONF_AUTH_TOKEN] = plex_auth.token + + # handle action Local auth (no MyPlex) + if action == CONF_ACTION_AUTH_LOCAL: + assert values + values[CONF_AUTH_TOKEN] = AUTH_TOKEN_UNAUTH + + # collect all config entries to show + entries: list[ConfigEntry] = [] + + # show GDM discovery (if we do not yet have any server details) + if values is None or not values.get(CONF_LOCAL_SERVER_IP): + entries.append( + ConfigEntry( + key=CONF_ACTION_GDM, + type=ConfigEntryType.ACTION, + label="Use Plex GDM to discover local servers", + description='Enable "GDM" to discover local Plex servers automatically.', + action=CONF_ACTION_GDM, + action_label="Use Plex GDM to discover local servers", + ) + ) + + # server details config entries (IP, port etc.) + entries += [ + ConfigEntry( + key=CONF_LOCAL_SERVER_IP, + type=ConfigEntryType.STRING, + label="Local server IP", + description="The local server IP (e.g. 192.168.1.77)", + required=True, + value=values.get(CONF_LOCAL_SERVER_IP) if values else None, + ), + ConfigEntry( + key=CONF_LOCAL_SERVER_PORT, + type=ConfigEntryType.INTEGER, + label="Local server port", + description="The local server port (e.g. 32400)", + required=True, + default_value=32400, + value=values.get(CONF_LOCAL_SERVER_PORT) if values else None, + ), + ConfigEntry( + key=CONF_LOCAL_SERVER_SSL, + type=ConfigEntryType.BOOLEAN, + label="SSL (HTTPS)", + description="Connect to the local server using SSL (HTTPS)", + required=True, + default_value=False, + ), + ConfigEntry( + key=CONF_LOCAL_SERVER_VERIFY_CERT, + type=ConfigEntryType.BOOLEAN, + label="Verify certificate", + description="Verify local server SSL certificate", + required=True, + default_value=True, + depends_on=CONF_LOCAL_SERVER_SSL, + category="advanced", + ), + ConfigEntry( + key=CONF_AUTH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label=CONF_AUTH_TOKEN, + action=CONF_AUTH_TOKEN, + value=values.get(CONF_AUTH_TOKEN) if values else None, + hidden=True, + ), + ] + + # config flow auth action/step to pick the library to use + # because this call is very slow, we only show/calculate the dropdown if we do + # not yet have this info or we/user invalidated it. + if values and values.get(CONF_AUTH_TOKEN): + conf_libraries = ConfigEntry( + key=CONF_LIBRARY_ID, + type=ConfigEntryType.STRING, + label="Library", + required=True, + description="The library to connect to (e.g. Music)", + depends_on=CONF_AUTH_TOKEN, + action=CONF_ACTION_LIBRARY, + action_label="Select Plex Music Library", + ) + if action in (CONF_ACTION_LIBRARY, CONF_ACTION_AUTH_MYPLEX, CONF_ACTION_AUTH_LOCAL): + token = mass.config.decrypt_string(str(values.get(CONF_AUTH_TOKEN))) + server_http_ip = str(values.get(CONF_LOCAL_SERVER_IP)) + server_http_port = str(values.get(CONF_LOCAL_SERVER_PORT)) + server_http_ssl = bool(values.get(CONF_LOCAL_SERVER_SSL)) + server_http_verify_cert = bool(values.get(CONF_LOCAL_SERVER_VERIFY_CERT)) + if not ( + libraries := await get_libraries( + mass, + token, + server_http_ssl, + server_http_ip, + server_http_port, + server_http_verify_cert, + ) + ): + msg = "Unable to retrieve Servers and/or Music Libraries" + raise LoginFailed(msg) + conf_libraries.options = tuple( + # use the same value for both the value and the title + # until we find out what plex uses as stable identifiers + ConfigValueOption( + title=x, + value=x, + ) + for x in libraries + ) + # select first library as (default) value + conf_libraries.default_value = libraries[0] + conf_libraries.value = libraries[0] + entries.append(conf_libraries) + + # show authentication options + if values is None or not values.get(CONF_AUTH_TOKEN): + entries.append( + ConfigEntry( + key=CONF_ACTION_AUTH_MYPLEX, + type=ConfigEntryType.ACTION, + label="Authenticate with MyPlex", + description="Authenticate with MyPlex to access your library.", + action=CONF_ACTION_AUTH_MYPLEX, + action_label="Authenticate with MyPlex", + ) + ) + entries.append( + ConfigEntry( + key=CONF_ACTION_AUTH_LOCAL, + type=ConfigEntryType.ACTION, + label="Authenticate locally", + description="Authenticate locally to access your library.", + action=CONF_ACTION_AUTH_LOCAL, + action_label="Authenticate locally", + ) + ) + else: + entries.append( + ConfigEntry( + key=CONF_ACTION_CLEAR_AUTH, + type=ConfigEntryType.ACTION, + label="Clear authentication", + description="Clear the current authentication details.", + action=CONF_ACTION_CLEAR_AUTH, + action_label="Clear authentication", + required=False, + ) + ) + + # return all config entries + return tuple(entries) + + +Param = ParamSpec("Param") +RetType = TypeVar("RetType") +PlexObjectT = TypeVar("PlexObjectT", bound=PlexObject) +MediaItemT = TypeVar("MediaItemT", bound=MediaItem) + + +class PlexProvider(MusicProvider): + """Provider for a plex music library.""" + + _plex_server: PlexServer = None + _plex_library: PlexMusicSection = None + _myplex_account: MyPlexAccount = None + _baseurl: str + + async def handle_async_init(self) -> None: + """Set up the music provider by connecting to the server.""" + # silence loggers + logging.getLogger("plexapi").setLevel(self.logger.level + 10) + _, library_name = str(self.config.get_value(CONF_LIBRARY_ID)).split(" / ", 1) + + def connect() -> PlexServer: + try: + session = requests.Session() + session.verify = ( + self.config.get_value(CONF_LOCAL_SERVER_VERIFY_CERT) + if self.config.get_value(CONF_LOCAL_SERVER_SSL) + else False + ) + local_server_protocol = ( + "https" if self.config.get_value(CONF_LOCAL_SERVER_SSL) else "http" + ) + token = self.config.get_value(CONF_AUTH_TOKEN) + plex_url = ( + f"{local_server_protocol}://{self.config.get_value(CONF_LOCAL_SERVER_IP)}" + f":{self.config.get_value(CONF_LOCAL_SERVER_PORT)}" + ) + if token == AUTH_TOKEN_UNAUTH: + # Doing local connection, not via plex.tv. + plex_server = PlexServer(plex_url) + else: + plex_server = PlexServer( + plex_url, + token, + session=session, + ) + # I don't think PlexAPI intends for this to be accessible, but we need it. + self._baseurl = plex_server._baseurl + + except plexapi.exceptions.BadRequest as err: + if "Invalid token" in str(err): + # token invalid, invalidate the config + self.mass.create_task( + self.mass.config.remove_provider_config_value( + self.instance_id, CONF_AUTH_TOKEN + ), + ) + msg = "Authentication failed" + raise LoginFailed(msg) + raise LoginFailed from err + return plex_server + + self._myplex_account = await self.get_myplex_account_and_refresh_token( + str(self.config.get_value(CONF_AUTH_TOKEN)) + ) + try: + self._plex_server = await self._run_async(connect) + self._plex_library = await self._run_async( + self._plex_server.library.section, library_name + ) + except requests.exceptions.ConnectionError as err: + raise SetupFailedError from err + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return a list of supported features.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ) + + @property + def is_streaming_provider(self) -> bool: + """ + Return True if the provider is a streaming provider. + + This literally means that the catalog is not the same as the library contents. + For local based providers (files, plex), the catalog is the same as the library content. + It also means that data is if this provider is NOT a streaming provider, + data cross instances is unique, the catalog and library differs per instance. + + Setting this to True will only query one instance of the provider for search and lookups. + Setting this to False will query all instances of this provider for search and lookups. + """ + return False + + async def resolve_image(self, path: str) -> str | bytes: + """Return the full image URL including the auth token.""" + return str(self._plex_server.url(path, True)) + + async def _run_async( + self, call: Callable[Param, RetType], *args: Param.args, **kwargs: Param.kwargs + ) -> RetType: + await self.get_myplex_account_and_refresh_token(str(self.config.get_value(CONF_AUTH_TOKEN))) + return await asyncio.to_thread(call, *args, **kwargs) + + async def _get_data(self, key: str, cls: type[PlexObjectT]) -> PlexObjectT: + results = await self._run_async(self._plex_library.fetchItem, key, cls) + return cast(PlexObjectT, results) + + def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: + name, version = parse_title_and_version(name) + if media_type in (MediaType.ALBUM, MediaType.TRACK): + name, version = parse_title_and_version(name) + else: + version = "" + return ItemMapping( + media_type=media_type, + item_id=key, + provider=self.instance_id, + name=name, + version=version, + ) + + async def _get_or_create_artist_by_name(self, artist_name: str) -> Artist | ItemMapping: + if library_items := await self.mass.music.artists._get_library_items_by_query( + search=artist_name, provider=self.instance_id + ): + return ItemMapping.from_item(library_items[0]) + + artist_id = FAKE_ARTIST_PREFIX + artist_name + return Artist( + item_id=artist_id, + name=artist_name or UNKNOWN_ARTIST, + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + + async def _parse(self, plex_media: PlexObject) -> MediaItem | None: + if plex_media.type == "artist": + return await self._parse_artist(plex_media) + elif plex_media.type == "album": + return await self._parse_album(plex_media) + elif plex_media.type == "track": + return await self._parse_track(plex_media) + elif plex_media.type == "playlist": + return await self._parse_playlist(plex_media) + return None + + async def _search_track(self, search_query: str | None, limit: int) -> list[PlexTrack]: + return cast( + list[PlexTrack], + await self._run_async(self._plex_library.searchTracks, title=search_query, limit=limit), + ) + + async def _search_album(self, search_query: str, limit: int) -> list[PlexAlbum]: + return cast( + list[PlexAlbum], + await self._run_async(self._plex_library.searchAlbums, title=search_query, limit=limit), + ) + + async def _search_artist(self, search_query: str, limit: int) -> list[PlexArtist]: + return cast( + list[PlexArtist], + await self._run_async( + self._plex_library.searchArtists, title=search_query, limit=limit + ), + ) + + async def _search_playlist(self, search_query: str, limit: int) -> list[PlexPlaylist]: + return cast( + list[PlexPlaylist], + await self._run_async(self._plex_library.playlists, title=search_query, limit=limit), + ) + + async def _search_track_advanced(self, limit: int, **kwargs: Any) -> list[PlexTrack]: + return cast( + list[PlexPlaylist], + await self._run_async(self._plex_library.searchTracks, filters=kwargs, limit=limit), + ) + + async def _search_album_advanced(self, limit: int, **kwargs: Any) -> list[PlexAlbum]: + return cast( + list[PlexPlaylist], + await self._run_async(self._plex_library.searchAlbums, filters=kwargs, limit=limit), + ) + + async def _search_artist_advanced(self, limit: int, **kwargs: Any) -> list[PlexArtist]: + return cast( + list[PlexPlaylist], + await self._run_async(self._plex_library.searchArtists, filters=kwargs, limit=limit), + ) + + async def _search_playlist_advanced(self, limit: int, **kwargs: Any) -> list[PlexPlaylist]: + return cast( + list[PlexPlaylist], + await self._run_async(self._plex_library.playlists, filters=kwargs, limit=limit), + ) + + async def _search_and_parse( + self, + search_coro: Awaitable[list[PlexObjectT]], + parse_coro: Callable[[PlexObjectT], Coroutine[Any, Any, MediaItemT]], + ) -> list[MediaItemT]: + task_results: list[Task[MediaItemT]] = [] + async with TaskGroup() as tg: + for item in await search_coro: + task_results.append(tg.create_task(parse_coro(item))) + + results: list[MediaItemT] = [] + for task in task_results: + results.append(task.result()) + + return results + + async def _parse_album(self, plex_album: PlexAlbum) -> Album: + """Parse a Plex Album response to an Album model object.""" + album_id = plex_album.key + album = Album( + item_id=album_id, + provider=self.domain, + name=plex_album.title or "[Unknown]", + provider_mappings={ + ProviderMapping( + item_id=str(album_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_album.getWebURL(self._baseurl), + ) + }, + ) + # Only add 5-star rated albums to Favorites. rating will be 10.0 for those. + # TODO: Let user set threshold? + with suppress(KeyError): + # suppress KeyError (as it doesn't exist for items without rating), + # allow sync to continue + album.favorite = plex_album._data.attrib["userRating"] == "10.0" + + if plex_album.year: + album.year = plex_album.year + if thumb := plex_album.firstAttr("thumb", "parentThumb", "grandparentThumb"): + album.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=thumb, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + ) + if plex_album.summary: + album.metadata.description = plex_album.summary + + album.artists.append( + self._get_item_mapping( + MediaType.ARTIST, + plex_album.parentKey, + plex_album.parentTitle or UNKNOWN_ARTIST, + ) + ) + return album + + async def _parse_artist(self, plex_artist: PlexArtist) -> Artist: + """Parse a Plex Artist response to Artist model object.""" + artist_id = plex_artist.key + if not artist_id: + msg = "Artist does not have a valid ID" + raise InvalidDataError(msg) + artist = Artist( + item_id=artist_id, + name=plex_artist.title or UNKNOWN_ARTIST, + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_artist.getWebURL(self._baseurl), + ) + }, + ) + if plex_artist.summary: + artist.metadata.description = plex_artist.summary + if thumb := plex_artist.firstAttr("thumb", "parentThumb", "grandparentThumb"): + artist.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=thumb, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + ) + return artist + + async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist: + """Parse a Plex Playlist response to a Playlist object.""" + playlist = Playlist( + item_id=plex_playlist.key, + provider=self.domain, + name=plex_playlist.title or "[Unknown]", + provider_mappings={ + ProviderMapping( + item_id=plex_playlist.key, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=plex_playlist.getWebURL(self._baseurl), + ) + }, + ) + if plex_playlist.summary: + playlist.metadata.description = plex_playlist.summary + if thumb := plex_playlist.firstAttr("thumb", "parentThumb", "grandparentThumb"): + playlist.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=thumb, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + ) + playlist.is_editable = not plex_playlist.smart + playlist.cache_checksum = str(plex_playlist.updatedAt.timestamp()) + + return playlist + + async def _parse_track(self, plex_track: PlexTrack) -> Track: + """Parse a Plex Track response to a Track model object.""" + if plex_track.media: + available = True + content = plex_track.media[0].container + else: + available = False + content = None + track = Track( + item_id=plex_track.key, + provider=self.instance_id, + name=plex_track.title or "[Unknown]", + provider_mappings={ + ProviderMapping( + item_id=plex_track.key, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=available, + audio_format=AudioFormat( + content_type=( + ContentType.try_parse(content) if content else ContentType.UNKNOWN + ), + ), + url=plex_track.getWebURL(self._baseurl), + ) + }, + disc_number=plex_track.parentIndex or 0, + track_number=plex_track.trackNumber or 0, + ) + # Only add 5-star rated tracks to Favorites. userRating will be 10.0 for those. + # TODO: Let user set threshold? + with suppress(KeyError): + # suppress KeyError (as it doesn't exist for items without rating), + # allow sync to continue + track.favorite = plex_track._data.attrib["userRating"] == "10.0" + + if plex_track.originalTitle and plex_track.originalTitle != plex_track.grandparentTitle: + # The artist of the track if different from the album's artist. + # For this kind of artist, we just know the name, so we create a fake artist, + # if it does not already exist. + track.artists.append( + await self._get_or_create_artist_by_name(plex_track.originalTitle or UNKNOWN_ARTIST) + ) + elif plex_track.grandparentKey: + track.artists.append( + self._get_item_mapping( + MediaType.ARTIST, + plex_track.grandparentKey, + plex_track.grandparentTitle or UNKNOWN_ARTIST, + ) + ) + else: + msg = "No artist was found for track" + raise InvalidDataError(msg) + + if thumb := plex_track.firstAttr("thumb", "parentThumb", "grandparentThumb"): + track.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=thumb, + provider=self.instance_id, + remotely_accessible=False, + ) + ] + ) + if plex_track.parentKey: + track.album = self._get_item_mapping( + MediaType.ALBUM, plex_track.parentKey, plex_track.parentTitle + ) + if plex_track.duration: + track.duration = int(plex_track.duration / 1000) + if plex_track.chapters: + track.metadata.chapters = UniqueList( + [ + MediaItemChapter( + chapter_id=plex_chapter.id, + position_start=plex_chapter.start, + position_end=plex_chapter.end, + title=plex_chapter.title, + ) + for plex_chapter in plex_track.chapters + ] + ) + + return track + + async def search( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 20, + ) -> SearchResults: + """Perform search on the plex library. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + artists = None + albums = None + tracks = None + playlists = None + + async with TaskGroup() as tg: + if MediaType.ARTIST in media_types: + artists = tg.create_task( + self._search_and_parse( + self._search_artist(search_query, limit), self._parse_artist + ) + ) + + if MediaType.ALBUM in media_types: + albums = tg.create_task( + self._search_and_parse( + self._search_album(search_query, limit), self._parse_album + ) + ) + + if MediaType.TRACK in media_types: + tracks = tg.create_task( + self._search_and_parse( + self._search_track(search_query, limit), self._parse_track + ) + ) + + if MediaType.PLAYLIST in media_types: + playlists = tg.create_task( + self._search_and_parse( + self._search_playlist(search_query, limit), + self._parse_playlist, + ) + ) + + search_results = SearchResults() + + if artists: + search_results.artists = artists.result() + + if albums: + search_results.albums = albums.result() + + if tracks: + search_results.tracks = tracks.result() + + if playlists: + search_results.playlists = playlists.result() + + return search_results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Plex Music.""" + artists_obj = await self._run_async(self._plex_library.all) + for artist in artists_obj: + yield await self._parse_artist(artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Plex Music.""" + albums_obj = await self._run_async(self._plex_library.albums) + for album in albums_obj: + yield await self._parse_album(album) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + playlists_obj = await self._run_async(self._plex_library.playlists) + for playlist in playlists_obj: + yield await self._parse_playlist(playlist) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Plex Music.""" + tracks_obj = await self._search_track(None, limit=99999) + for track in tracks_obj: + yield await self._parse_track(track) + + async def get_album(self, prov_album_id: str) -> Album: + """Get full album details by id.""" + if plex_album := await self._get_data(prov_album_id, PlexAlbum): + return await self._parse_album(plex_album) + msg = f"Item {prov_album_id} not found" + raise MediaNotFoundError(msg) + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + plex_album: PlexAlbum = await self._get_data(prov_album_id, PlexAlbum) + tracks = [] + for plex_track in await self._run_async(plex_album.tracks): + track = await self._parse_track( + plex_track, + ) + tracks.append(track) + return tracks + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + if prov_artist_id.startswith(FAKE_ARTIST_PREFIX): + # This artist does not exist in plex, so we can just load it from DB. + + if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( + prov_artist_id, self.instance_id + ): + return db_artist + msg = f"Artist not found: {prov_artist_id}" + raise MediaNotFoundError(msg) + + if plex_artist := await self._get_data(prov_artist_id, PlexArtist): + return await self._parse_artist(plex_artist) + msg = f"Item {prov_artist_id} not found" + raise MediaNotFoundError(msg) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + if plex_track := await self._get_data(prov_track_id, PlexTrack): + return await self._parse_track(plex_track) + msg = f"Item {prov_track_id} not found" + raise MediaNotFoundError(msg) + + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get full playlist details by id.""" + if plex_playlist := await self._get_data(prov_playlist_id, PlexPlaylist): + return await self._parse_playlist(plex_playlist) + msg = f"Item {prov_playlist_id} not found" + raise MediaNotFoundError(msg) + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # paging not supported, we always return the whole list at once + return [] + plex_playlist: PlexPlaylist = await self._get_data(prov_playlist_id, PlexPlaylist) + if not (playlist_items := await self._run_async(plex_playlist.items)): + return result + for index, plex_track in enumerate(playlist_items, 1): + if track := await self._parse_track(plex_track): + track.position = index + result.append(track) + return result + + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get a list of albums for the given artist.""" + if not prov_artist_id.startswith(FAKE_ARTIST_PREFIX): + plex_artist = await self._get_data(prov_artist_id, PlexArtist) + plex_albums = cast(list[PlexAlbum], await self._run_async(plex_artist.albums)) + if plex_albums: + albums = [] + for album_obj in plex_albums: + albums.append(await self._parse_album(album_obj)) + return albums + return [] + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track.""" + plex_track = await self._get_data(item_id, PlexTrack) + if not plex_track or not plex_track.media: + msg = f"track {item_id} not found" + raise MediaNotFoundError(msg) + + media: PlexMedia = plex_track.media[0] + + media_type = ( + ContentType.try_parse(media.container) if media.container else ContentType.UNKNOWN + ) + media_part: PlexMediaPart = media.parts[0] + audio_stream: PlexAudioStream = media_part.audioStreams()[0] + + stream_details = StreamDetails( + item_id=plex_track.key, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=media_type, + channels=media.audioChannels, + ), + stream_type=StreamType.HTTP, + duration=plex_track.duration, + data=plex_track, + ) + + if media_type != ContentType.M4A: + stream_details.path = self._plex_server.url(media_part.key, True) + if audio_stream.samplingRate: + stream_details.audio_format.sample_rate = audio_stream.samplingRate + if audio_stream.bitDepth: + stream_details.audio_format.bit_depth = audio_stream.bitDepth + + else: + url = plex_track.getStreamURL() + media_info = await parse_tags(url) + stream_details.path = url + stream_details.audio_format.channels = media_info.channels + stream_details.audio_format.content_type = ContentType.try_parse(media_info.format) + stream_details.audio_format.sample_rate = media_info.sample_rate + stream_details.audio_format.bit_depth = media_info.bits_per_sample + + return stream_details + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + + def mark_played() -> None: + item = streamdetails.data + params = {"key": str(item.ratingKey), "identifier": "com.plexapp.plugins.library"} + self._plex_server.query("/:/scrobble", params=params) + + await asyncio.to_thread(mark_played) + + async def get_myplex_account_and_refresh_token(self, auth_token: str) -> MyPlexAccount: + """Get a MyPlexAccount object and refresh the token if needed.""" + if auth_token == AUTH_TOKEN_UNAUTH: + return self._myplex_account + + def _refresh_plex_token() -> MyPlexAccount: + if self._myplex_account is None: + myplex_account = MyPlexAccount(token=auth_token) + self._myplex_account = myplex_account + self._myplex_account.ping() + return self._myplex_account + + return await asyncio.to_thread(_refresh_plex_token) diff --git a/music_assistant/providers/plex/helpers.py b/music_assistant/providers/plex/helpers.py new file mode 100644 index 00000000..0c303bcc --- /dev/null +++ b/music_assistant/providers/plex/helpers.py @@ -0,0 +1,81 @@ +"""Several helpers/utils for the Plex Music Provider.""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING, cast + +import requests +from plexapi.gdm import GDM +from plexapi.library import LibrarySection as PlexLibrarySection +from plexapi.library import MusicSection as PlexMusicSection +from plexapi.server import PlexServer + +if TYPE_CHECKING: + from music_assistant import MusicAssistant + + +async def get_libraries( + mass: MusicAssistant, + auth_token: str | None, + local_server_ssl: bool, + local_server_ip: str, + local_server_port: str, + local_server_verify_cert: bool, +) -> list[str]: + """ + Get all music libraries for all plex servers. + + Returns a dict of Library names in format {'servername / library name':'baseurl'} + """ + cache_key = "plex_libraries" + + def _get_libraries() -> list[str]: + # create a listing of available music libraries on all servers + all_libraries: list[str] = [] + session = requests.Session() + session.verify = local_server_verify_cert + local_server_protocol = "https" if local_server_ssl else "http" + plex_server: PlexServer + if auth_token is None: + plex_server = PlexServer( + f"{local_server_protocol}://{local_server_ip}:{local_server_port}" + ) + else: + plex_server = PlexServer( + f"{local_server_protocol}://{local_server_ip}:{local_server_port}", + auth_token, + session=session, + ) + for media_section in cast(list[PlexLibrarySection], plex_server.library.sections()): + if media_section.type != PlexMusicSection.TYPE: + continue + # TODO: figure out what plex uses as stable id and use that instead of names + all_libraries.append(f"{plex_server.friendlyName} / {media_section.title}") + return all_libraries + + if cache := await mass.cache.get(cache_key, checksum=auth_token): + return cast(list[str], cache) + + result = await asyncio.to_thread(_get_libraries) + # use short expiration for in-memory cache + await mass.cache.set(cache_key, result, checksum=auth_token, expiration=3600) + return result + + +async def discover_local_servers() -> tuple[str, int] | tuple[None, None]: + """Discover all local plex servers on the network.""" + + def _discover_local_servers() -> tuple[str, int] | tuple[None, None]: + gdm = GDM() + gdm.scan() + if len(gdm.entries) > 0: + entry = gdm.entries[0] + data = entry.get("data") + local_server_ip = entry.get("from")[0] + local_server_port = data.get("Port") + return local_server_ip, local_server_port + else: + return None, None + + return await asyncio.to_thread(_discover_local_servers) diff --git a/music_assistant/providers/plex/icon.svg b/music_assistant/providers/plex/icon.svg new file mode 100644 index 00000000..7c994b84 --- /dev/null +++ b/music_assistant/providers/plex/icon.svg @@ -0,0 +1,4 @@ + diff --git a/music_assistant/providers/plex/manifest.json b/music_assistant/providers/plex/manifest.json new file mode 100644 index 00000000..9ced2037 --- /dev/null +++ b/music_assistant/providers/plex/manifest.json @@ -0,0 +1,14 @@ +{ + "type": "music", + "domain": "plex", + "name": "Plex Media Server Library", + "description": "Support for the Plex streaming provider in Music Assistant.", + "codeowners": [ + "@micha91" + ], + "requirements": [ + "plexapi==4.15.16" + ], + "documentation": "https://music-assistant.io/music-providers/plex/", + "multi_instance": true +} diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py new file mode 100644 index 00000000..da407d36 --- /dev/null +++ b/music_assistant/providers/qobuz/__init__.py @@ -0,0 +1,824 @@ +"""Qobuz musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +import datetime +import hashlib +import time +from contextlib import suppress +from typing import TYPE_CHECKING + +from aiohttp import client_exceptions +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import ( + InvalidDataError, + LoginFailed, + MediaNotFoundError, + ResourceTemporarilyUnavailable, +) +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + AudioFormat, + ContentType, + ImageType, + MediaItemImage, + MediaItemType, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import ( + CONF_PASSWORD, + CONF_USERNAME, + VARIOUS_ARTISTS_MBID, + VARIOUS_ARTISTS_NAME, +) +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import lock, parse_title_and_version, try_parse_int +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, +) + +VARIOUS_ARTISTS_ID = "145383" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return QobuzProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + ), + ConfigEntry( + key=CONF_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password", + required=True, + ), + ) + + +class QobuzProvider(MusicProvider): + """Provider for the Qobux music service.""" + + _user_auth_info: str | None = None + # rate limiter needs to be specified on provider-level, + # so make it an instance attribute + throttler = ThrottlerManager(rate_limit=1, period=2) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + if not self.config.get_value(CONF_USERNAME) or not self.config.get_value(CONF_PASSWORD): + msg = "Invalid login credentials" + raise LoginFailed(msg) + # try to get a token, raise if that fails + token = await self._auth_token() + if not token: + msg = f"Login failed for user {self.config.get_value(CONF_USERNAME)}" + raise LoginFailed(msg) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def search( + self, search_query: str, media_types=list[MediaType], limit: int = 5 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + result = SearchResults() + media_types = [ + x + for x in media_types + if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return result + params = {"query": search_query, "limit": limit} + if len(media_types) == 1: + # qobuz does not support multiple searchtypes, falls back to all if no type given + if media_types[0] == MediaType.ARTIST: + params["type"] = "artists" + if media_types[0] == MediaType.ALBUM: + params["type"] = "albums" + if media_types[0] == MediaType.TRACK: + params["type"] = "tracks" + if media_types[0] == MediaType.PLAYLIST: + params["type"] = "playlists" + if searchresult := await self._get_data("catalog/search", **params): + if "artists" in searchresult and MediaType.ARTIST in media_types: + result.artists += [ + self._parse_artist(item) + for item in searchresult["artists"]["items"] + if (item and item["id"]) + ] + if "albums" in searchresult and MediaType.ALBUM in media_types: + result.albums += [ + await self._parse_album(item) + for item in searchresult["albums"]["items"] + if (item and item["id"]) + ] + if "tracks" in searchresult and MediaType.TRACK in media_types: + result.tracks += [ + await self._parse_track(item) + for item in searchresult["tracks"]["items"] + if (item and item["id"]) + ] + if "playlists" in searchresult and MediaType.PLAYLIST in media_types: + result.playlists += [ + self._parse_playlist(item) + for item in searchresult["playlists"]["items"] + if (item and item["id"]) + ] + return result + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Qobuz.""" + endpoint = "favorite/getUserFavorites" + for item in await self._get_all_items(endpoint, key="artists", type="artists"): + if item and item["id"]: + yield self._parse_artist(item) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Qobuz.""" + endpoint = "favorite/getUserFavorites" + for item in await self._get_all_items(endpoint, key="albums", type="albums"): + if item and item["id"]: + yield await self._parse_album(item) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Qobuz.""" + endpoint = "favorite/getUserFavorites" + for item in await self._get_all_items(endpoint, key="tracks", type="tracks"): + if item and item["id"]: + yield await self._parse_track(item) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + endpoint = "playlist/getUserPlaylists" + for item in await self._get_all_items(endpoint, key="playlists"): + if item and item["id"]: + yield self._parse_playlist(item) + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + params = {"artist_id": prov_artist_id} + if (artist_obj := await self._get_data("artist/get", **params)) and artist_obj["id"]: + return self._parse_artist(artist_obj) + msg = f"Item {prov_artist_id} not found" + raise MediaNotFoundError(msg) + + async def get_album(self, prov_album_id) -> Album: + """Get full album details by id.""" + params = {"album_id": prov_album_id} + if (album_obj := await self._get_data("album/get", **params)) and album_obj["id"]: + return await self._parse_album(album_obj) + msg = f"Item {prov_album_id} not found" + raise MediaNotFoundError(msg) + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + params = {"track_id": prov_track_id} + if (track_obj := await self._get_data("track/get", **params)) and track_obj["id"]: + return await self._parse_track(track_obj) + msg = f"Item {prov_track_id} not found" + raise MediaNotFoundError(msg) + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + params = {"playlist_id": prov_playlist_id} + if (playlist_obj := await self._get_data("playlist/get", **params)) and playlist_obj["id"]: + return self._parse_playlist(playlist_obj) + msg = f"Item {prov_playlist_id} not found" + raise MediaNotFoundError(msg) + + async def get_album_tracks(self, prov_album_id) -> list[Track]: + """Get all album tracks for given album id.""" + params = {"album_id": prov_album_id} + return [ + await self._parse_track(item) + for item in await self._get_all_items("album/get", **params, key="tracks") + if (item and item["id"]) + ] + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + page_size = 100 + offset = page * page_size + qobuz_result = await self._get_data( + "playlist/get", + key="tracks", + playlist_id=prov_playlist_id, + extra="tracks", + offset=offset, + limit=page_size, + ) + for index, track_obj in enumerate(qobuz_result["tracks"]["items"], 1): + if not (track_obj and track_obj["id"]): + continue + track = await self._parse_track(track_obj) + track.position = index + offset + result.append(track) + return result + + async def get_artist_albums(self, prov_artist_id) -> list[Album]: + """Get a list of albums for the given artist.""" + result = await self._get_data( + "artist/get", + artist_id=prov_artist_id, + extra="albums", + offset=0, + limit=100, + ) + return [ + await self._parse_album(item) + for item in result["albums"]["items"] + if (item and item["id"] and str(item["artist"]["id"]) == prov_artist_id) + ] + + async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: + """Get a list of most popular tracks for the given artist.""" + result = await self._get_data( + "artist/get", + artist_id=prov_artist_id, + extra="playlists", + offset=0, + limit=25, + ) + if result and result["playlists"]: + return [ + await self._parse_track(item) + for item in result["playlists"][0]["tracks"]["items"] + if (item and item["id"]) + ] + # fallback to search + artist = await self.get_artist(prov_artist_id) + searchresult = await self._get_data( + "catalog/search", query=artist.name, limit=25, type="tracks" + ) + return [ + await self._parse_track(item) + for item in searchresult["tracks"]["items"] + if ( + item + and item["id"] + and "performer" in item + and str(item["performer"]["id"]) == str(prov_artist_id) + ) + ] + + async def get_similar_artists(self, prov_artist_id) -> None: + """Get similar artists for given artist.""" + # https://www.qobuz.com/api.json/0.2/artist/getSimilarArtists?artist_id=220020&offset=0&limit=3 + + async def library_add(self, item: MediaItemType): + """Add item to library.""" + result = None + if item.media_type == MediaType.ARTIST: + result = await self._get_data("favorite/create", artist_id=item.item_id) + elif item.media_type == MediaType.ALBUM: + result = await self._get_data("favorite/create", album_ids=item.item_id) + elif item.media_type == MediaType.TRACK: + result = await self._get_data("favorite/create", track_ids=item.item_id) + elif item.media_type == MediaType.PLAYLIST: + result = await self._get_data("playlist/subscribe", playlist_id=item.item_id) + return result + + async def library_remove(self, prov_item_id, media_type: MediaType): + """Remove item from library.""" + result = None + if media_type == MediaType.ARTIST: + result = await self._get_data("favorite/delete", artist_ids=prov_item_id) + elif media_type == MediaType.ALBUM: + result = await self._get_data("favorite/delete", album_ids=prov_item_id) + elif media_type == MediaType.TRACK: + result = await self._get_data("favorite/delete", track_ids=prov_item_id) + elif media_type == MediaType.PLAYLIST: + playlist = await self.get_playlist(prov_item_id) + if playlist.is_editable: + result = await self._get_data("playlist/delete", playlist_id=prov_item_id) + else: + result = await self._get_data("playlist/unsubscribe", playlist_id=prov_item_id) + return result + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + return await self._get_data( + "playlist/addTracks", + playlist_id=prov_playlist_id, + track_ids=",".join(prov_track_ids), + playlist_track_ids=",".join(prov_track_ids), + ) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int] + ) -> None: + """Remove track(s) from playlist.""" + playlist_track_ids = set() + for pos in positions_to_remove: + idx = pos - 1 + qobuz_result = await self._get_data( + "playlist/get", + key="tracks", + playlist_id=prov_playlist_id, + extra="tracks", + offset=idx, + limit=1, + ) + if not qobuz_result: + continue + playlist_track_id = qobuz_result["tracks"]["items"][0]["playlist_track_id"] + playlist_track_ids.add(str(playlist_track_id)) + + return await self._get_data( + "playlist/deleteTracks", + playlist_id=prov_playlist_id, + playlist_track_ids=",".join(playlist_track_ids), + ) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + streamdata = None + for format_id in [27, 7, 6, 5]: + # it seems that simply requesting for highest available quality does not work + # from time to time the api response is empty for this request ?! + result = await self._get_data( + "track/getFileUrl", + sign_request=True, + format_id=format_id, + track_id=item_id, + intent="stream", + ) + if result and result.get("url"): + streamdata = result + break + if not streamdata: + msg = f"Unable to retrieve stream details for {item_id}" + raise MediaNotFoundError(msg) + if streamdata["mime_type"] == "audio/mpeg": + content_type = ContentType.MPEG + elif streamdata["mime_type"] == "audio/flac": + content_type = ContentType.FLAC + else: + msg = f"Unsupported mime type for {item_id}" + raise MediaNotFoundError(msg) + self.mass.create_task(self._report_playback_started(streamdata)) + return StreamDetails( + item_id=str(item_id), + provider=self.instance_id, + audio_format=AudioFormat( + content_type=content_type, + sample_rate=int(streamdata["sampling_rate"] * 1000), + bit_depth=streamdata["bit_depth"], + ), + stream_type=StreamType.HTTP, + duration=streamdata["duration"], + data=streamdata, # we need these details for reporting playback + path=streamdata["url"], + ) + + async def _report_playback_started(self, streamdata: dict) -> None: + """Report playback start to qobuz.""" + # TODO: need to figure out if the streamed track is purchased by user + # https://www.qobuz.com/api.json/0.2/purchase/getUserPurchasesIds?limit=5000&user_id=xxxxxxx + # {"albums":{"total":0,"items":[]}, + # "tracks":{"total":0,"items":[]},"user":{"id":xxxx,"login":"xxxxx"}} + device_id = self._user_auth_info["user"]["device"]["id"] + credential_id = self._user_auth_info["user"]["credential"]["id"] + user_id = self._user_auth_info["user"]["id"] + format_id = streamdata["format_id"] + timestamp = int(time.time()) + events = [ + { + "online": True, + "sample": False, + "intent": "stream", + "device_id": device_id, + "track_id": streamdata["track_id"], + "purchase": False, + "date": timestamp, + "credential_id": credential_id, + "user_id": user_id, + "local": False, + "format_id": format_id, + } + ] + async with self.throttler.bypass(): + await self._post_data("track/reportStreamingStart", data=events) + + async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: + """Handle callback when an item completed streaming.""" + user_id = self._user_auth_info["user"]["id"] + async with self.throttler.bypass(): + await self._get_data( + "/track/reportStreamingEnd", + user_id=user_id, + track_id=str(streamdetails.item_id), + duration=try_parse_int(seconds_streamed), + ) + + def _parse_artist(self, artist_obj: dict): + """Parse qobuz artist object to generic layout.""" + artist = Artist( + item_id=str(artist_obj["id"]), + provider=self.domain, + name=artist_obj["name"], + provider_mappings={ + ProviderMapping( + item_id=str(artist_obj["id"]), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f'https://open.qobuz.com/artist/{artist_obj["id"]}', + ) + }, + ) + if artist.item_id == VARIOUS_ARTISTS_ID: + artist.mbid = VARIOUS_ARTISTS_MBID + artist.name = VARIOUS_ARTISTS_NAME + if img := self.__get_image(artist_obj): + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if artist_obj.get("biography"): + artist.metadata.description = artist_obj["biography"].get("content") + return artist + + async def _parse_album(self, album_obj: dict, artist_obj: dict | None = None): + """Parse qobuz album object to generic layout.""" + if not artist_obj and "artist" not in album_obj: + # artist missing in album info, return full abum instead + return await self.get_album(album_obj["id"]) + name, version = parse_title_and_version(album_obj["title"], album_obj.get("version")) + album = Album( + item_id=str(album_obj["id"]), + provider=self.domain, + name=name, + version=version, + provider_mappings={ + ProviderMapping( + item_id=str(album_obj["id"]), + provider_domain=self.domain, + provider_instance=self.instance_id, + available=album_obj["streamable"] and album_obj["displayable"], + audio_format=AudioFormat( + content_type=ContentType.FLAC, + sample_rate=album_obj["maximum_sampling_rate"] * 1000, + bit_depth=album_obj["maximum_bit_depth"], + ), + url=f'https://open.qobuz.com/album/{album_obj["id"]}', + ) + }, + ) + album.external_ids.add((ExternalID.BARCODE, album_obj["upc"])) + album.artists.append(self._parse_artist(artist_obj or album_obj["artist"])) + if ( + album_obj.get("product_type", "") == "single" + or album_obj.get("release_type", "") == "single" + ): + album.album_type = AlbumType.SINGLE + elif ( + album_obj.get("product_type", "") == "compilation" or "Various" in album.artists[0].name + ): + album.album_type = AlbumType.COMPILATION + elif ( + album_obj.get("product_type", "") == "album" + or album_obj.get("release_type", "") == "album" + ): + album.album_type = AlbumType.ALBUM + if "genre" in album_obj: + album.metadata.genres = {album_obj["genre"]["name"]} + if img := self.__get_image(album_obj): + album.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if "label" in album_obj: + album.metadata.label = album_obj["label"]["name"] + if released_at := album_obj.get("released_at"): + with suppress(ValueError): + album.year = datetime.datetime.fromtimestamp(released_at).year + if album_obj.get("copyright"): + album.metadata.copyright = album_obj["copyright"] + if album_obj.get("description"): + album.metadata.description = album_obj["description"] + if album_obj.get("parental_warning"): + album.metadata.explicit = True + return album + + async def _parse_track(self, track_obj: dict) -> Track: + """Parse qobuz track object to generic layout.""" + name, version = parse_title_and_version(track_obj["title"], track_obj.get("version")) + track = Track( + item_id=str(track_obj["id"]), + provider=self.domain, + name=name, + version=version, + duration=track_obj["duration"], + provider_mappings={ + ProviderMapping( + item_id=str(track_obj["id"]), + provider_domain=self.domain, + provider_instance=self.instance_id, + available=track_obj["streamable"] and track_obj["displayable"], + audio_format=AudioFormat( + content_type=ContentType.FLAC, + sample_rate=track_obj["maximum_sampling_rate"] * 1000, + bit_depth=track_obj["maximum_bit_depth"], + ), + url=f'https://open.qobuz.com/track/{track_obj["id"]}', + ) + }, + disc_number=track_obj.get("media_number", 0), + track_number=track_obj.get("track_number", 0), + ) + if isrc := track_obj.get("isrc"): + track.external_ids.add((ExternalID.ISRC, isrc)) + if track_obj.get("performer") and "Various " not in track_obj["performer"]: + artist = self._parse_artist(track_obj["performer"]) + if artist: + track.artists.append(artist) + # try to grab artist from album + if not track.artists and ( + track_obj.get("album") + and track_obj["album"].get("artist") + and "Various " not in track_obj["album"]["artist"] + ): + artist = self._parse_artist(track_obj["album"]["artist"]) + if artist: + track.artists.append(artist) + if not track.artists: + # last resort: parse from performers string + for performer_str in track_obj["performers"].split(" - "): + role = performer_str.split(", ")[1] + name = performer_str.split(", ")[0] + if "artist" in role.lower(): + artist = Artist( + item_id=name, + provider=self.domain, + name=name, + provider_mappings={ + ProviderMapping( + item_id=name, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + track.artists.append(artist) + # TODO: fix grabbing composer from details + + if "album" in track_obj: + album = await self._parse_album(track_obj["album"]) + if album: + track.album = album + if track_obj.get("performers"): + track.metadata.performers = {x.strip() for x in track_obj["performers"].split("-")} + if track_obj.get("copyright"): + track.metadata.copyright = track_obj["copyright"] + if track_obj.get("parental_warning"): + track.metadata.explicit = True + if img := self.__get_image(track_obj): + track.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + + return track + + def _parse_playlist(self, playlist_obj): + """Parse qobuz playlist object to generic layout.""" + playlist = Playlist( + item_id=str(playlist_obj["id"]), + provider=self.domain, + name=playlist_obj["name"], + owner=playlist_obj["owner"]["name"], + provider_mappings={ + ProviderMapping( + item_id=str(playlist_obj["id"]), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f'https://open.qobuz.com/playlist/{playlist_obj["id"]}', + ) + }, + ) + playlist.is_editable = ( + playlist_obj["owner"]["id"] == self._user_auth_info["user"]["id"] + or playlist_obj["is_collaborative"] + ) + if img := self.__get_image(playlist_obj): + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + playlist.cache_checksum = str(playlist_obj["updated_at"]) + return playlist + + @lock + async def _auth_token(self): + """Login to qobuz and store the token.""" + if self._user_auth_info: + return self._user_auth_info["user_auth_token"] + params = { + "username": self.config.get_value(CONF_USERNAME), + "password": self.config.get_value(CONF_PASSWORD), + "device_manufacturer_id": "music_assistant", + } + details = await self._get_data("user/login", **params) + if details and "user" in details: + self._user_auth_info = details + self.logger.info( + "Successfully logged in to Qobuz as %s", details["user"]["display_name"] + ) + self.mass.metadata.set_default_preferred_language(details["user"]["country_code"]) + return details["user_auth_token"] + return None + + async def _get_all_items(self, endpoint, key="tracks", **kwargs): + """Get all items from a paged list.""" + limit = 50 + offset = 0 + all_items = [] + while True: + kwargs["limit"] = limit + kwargs["offset"] = offset + result = await self._get_data(endpoint, **kwargs) + offset += limit + if not result: + break + if not result.get(key) or not result[key].get("items"): + break + for item in result[key]["items"]: + all_items.append(item) + if len(result[key]["items"]) < limit: + break + return all_items + + @throttle_with_retries + async def _get_data(self, endpoint, sign_request=False, **kwargs): + """Get data from api.""" + self.logger.debug("Handling GET request to %s", endpoint) + url = f"http://www.qobuz.com/api.json/0.2/{endpoint}" + headers = {"X-App-Id": app_var(0)} + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" + if endpoint != "user/login": + auth_token = await self._auth_token() + if not auth_token: + self.logger.debug("Not logged in") + return None + headers["X-User-Auth-Token"] = auth_token + if sign_request: + signing_data = "".join(endpoint.split("/")) + keys = list(kwargs.keys()) + keys.sort() + for key in keys: + signing_data += f"{key}{kwargs[key]}" + request_ts = str(time.time()) + request_sig = signing_data + request_ts + app_var(1) + request_sig = str(hashlib.md5(request_sig.encode()).hexdigest()) + kwargs["request_ts"] = request_ts + kwargs["request_sig"] = request_sig + kwargs["app_id"] = app_var(0) + kwargs["user_auth_token"] = await self._auth_token() + async with ( + self.mass.http_session.get(url, headers=headers, params=kwargs) as response, + ): + # handle rate limiter + if response.status == 429: + backoff_time = int(response.headers.get("Retry-After", 0)) + raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + # handle 404 not found, convert to MediaNotFoundError + if response.status == 404: + raise MediaNotFoundError(f"{endpoint} not found") + response.raise_for_status() + try: + return await response.json(loads=json_loads) + except client_exceptions.ContentTypeError as err: + text = err.message or await response.text() or err.status + msg = f"Error while handling {endpoint}: {text}" + raise InvalidDataError(msg) + + @throttle_with_retries + async def _post_data(self, endpoint, params=None, data=None): + """Post data to api.""" + self.logger.debug("Handling POST request to %s", endpoint) + if not params: + params = {} + if not data: + data = {} + url = f"http://www.qobuz.com/api.json/0.2/{endpoint}" + params["app_id"] = app_var(0) + params["user_auth_token"] = await self._auth_token() + async with self.mass.http_session.post( + url, params=params, json=data, ssl=False + ) as response: + # handle rate limiter + if response.status == 429: + backoff_time = int(response.headers.get("Retry-After", 0)) + raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + # handle 404 not found, convert to MediaNotFoundError + if response.status == 404: + raise MediaNotFoundError(f"{endpoint} not found") + response.raise_for_status() + return await response.json(loads=json_loads) + + def __get_image(self, obj: dict) -> str | None: + """Try to parse image from Qobuz media object.""" + if obj.get("image"): + for key in ["extralarge", "large", "medium", "small"]: + if obj["image"].get(key): + if "2a96cbd8b46e442fc41c2b86b821562f" in obj["image"][key]: + continue + return obj["image"][key] + if obj.get("images300"): + # playlists seem to use this strange format + return obj["images300"][0] + if obj.get("album"): + return self.__get_image(obj["album"]) + if obj.get("artist"): + return self.__get_image(obj["artist"]) + return None diff --git a/music_assistant/providers/qobuz/icon.svg b/music_assistant/providers/qobuz/icon.svg new file mode 100644 index 00000000..38d3349f --- /dev/null +++ b/music_assistant/providers/qobuz/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/music_assistant/providers/qobuz/icon_dark.svg b/music_assistant/providers/qobuz/icon_dark.svg new file mode 100644 index 00000000..2fc68c88 --- /dev/null +++ b/music_assistant/providers/qobuz/icon_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/music_assistant/providers/qobuz/manifest.json b/music_assistant/providers/qobuz/manifest.json new file mode 100644 index 00000000..61f77022 --- /dev/null +++ b/music_assistant/providers/qobuz/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "qobuz", + "name": "Qobuz", + "description": "Qobuz support for Music Assistant: Lossless (and hi-res) Music provider.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/music-providers/qobuz/", + "multi_instance": true +} diff --git a/music_assistant/providers/radiobrowser/__init__.py b/music_assistant/providers/radiobrowser/__init__.py new file mode 100644 index 00000000..203cb449 --- /dev/null +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -0,0 +1,362 @@ +"""RadioBrowser musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator, Sequence +from typing import TYPE_CHECKING, cast + +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, LinkType, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError +from music_assistant_models.media_items import ( + AudioFormat, + BrowseFolder, + ContentType, + ImageType, + MediaItemImage, + MediaItemLink, + MediaItemType, + MediaType, + ProviderMapping, + Radio, + SearchResults, + UniqueList, +) +from music_assistant_models.streamdetails import StreamDetails +from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station + +from music_assistant.controllers.cache import use_cache +from music_assistant.models.music_provider import MusicProvider + +SUPPORTED_FEATURES = ( + ProviderFeature.SEARCH, + ProviderFeature.BROWSE, + # RadioBrowser doesn't support a library feature at all + # but MA users like to favorite their radio stations and + # have that included in backups so we store it in the config. + ProviderFeature.LIBRARY_RADIOS, + ProviderFeature.LIBRARY_RADIOS_EDIT, +) + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +CONF_STORED_RADIOS = "stored_radios" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return RadioBrowserProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 D205 + return ( + ConfigEntry( + # RadioBrowser doesn't support a library feature at all + # but MA users like to favorite their radio stations and + # have that included in backups so we store it in the config. + key=CONF_STORED_RADIOS, + type=ConfigEntryType.STRING, + label=CONF_STORED_RADIOS, + default_value=[], + required=False, + multi_value=True, + hidden=True, + ), + ) + + +class RadioBrowserProvider(MusicProvider): + """Provider implementation for RadioBrowser.""" + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.radios = RadioBrowser( + session=self.mass.http_session, user_agent=f"MusicAssistant/{self.mass.version}" + ) + try: + # Try to get some stats to check connection to RadioBrowser API + await self.radios.stats() + except RadioBrowserError as err: + self.logger.exception("%s", err) + + # copy the radiobrowser items that were added to the library + # TODO: remove this logic after version 2.3.0 or later + if not self.config.get_value(CONF_STORED_RADIOS) and self.mass.music.database: + async for db_row in self.mass.music.database.iter_items( + "provider_mappings", + {"media_type": "radio", "provider_domain": "radiobrowser"}, + ): + await self.library_add(await self.get_radio(db_row["provider_item_id"])) + + async def search( + self, search_query: str, media_types: list[MediaType], limit: int = 10 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + result = SearchResults() + if MediaType.RADIO not in media_types: + return result + + searchresult = await self.radios.search(name=search_query, limit=limit) + result.radio = [await self._parse_radio(item) for item in searchresult] + + return result + + async def browse(self, path: str) -> Sequence[MediaItemType]: + """Browse this provider's items. + + :param path: The path to browse, (e.g. provid://artists). + """ + part_parts = path.split("://")[1].split("/") + subpath = part_parts[0] if part_parts else "" + subsubpath = part_parts[1] if len(part_parts) > 1 else "" + + if not subpath: + # return main listing + return [ + BrowseFolder( + item_id="popular", + provider=self.domain, + path=path + "popular", + name="", + label="radiobrowser_by_popularity", + ), + BrowseFolder( + item_id="country", + provider=self.domain, + path=path + "country", + name="", + label="radiobrowser_by_country", + ), + BrowseFolder( + item_id="tag", + provider=self.domain, + path=path + "tag", + name="", + label="radiobrowser_by_tag", + ), + ] + + if subpath == "popular": + return await self.get_by_popularity() + + if subpath == "tag" and subsubpath: + return await self.get_by_tag(subsubpath) + + if subpath == "tag": + return await self.get_tag_folders(path) + + if subpath == "country" and subsubpath: + return await self.get_by_country(subsubpath) + + if subpath == "country": + return await self.get_country_folders(path) + + return [] + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + stored_radios = self.config.get_value(CONF_STORED_RADIOS) + if TYPE_CHECKING: + stored_radios = cast(list[str], stored_radios) + for item in stored_radios: + yield await self.get_radio(item) + + async def library_add(self, item: MediaItemType) -> bool: + """Add item to provider's library. Return true on success.""" + stored_radios = self.config.get_value(CONF_STORED_RADIOS) + if TYPE_CHECKING: + stored_radios = cast(list[str], stored_radios) + if item.item_id in stored_radios: + return False + self.logger.debug("Adding radio %s to stored radios", item.item_id) + stored_radios = [*stored_radios, item.item_id] + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_STORED_RADIOS, stored_radios + ) + return True + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove item from provider's library. Return true on success.""" + stored_radios = self.config.get_value(CONF_STORED_RADIOS) + if TYPE_CHECKING: + stored_radios = cast(list[str], stored_radios) + if prov_item_id not in stored_radios: + return False + self.logger.debug("Removing radio %s from stored radios", prov_item_id) + stored_radios = [x for x in stored_radios if x != prov_item_id] + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_STORED_RADIOS, stored_radios + ) + return True + + @use_cache(3600 * 24) + async def get_tag_folders(self, base_path: str) -> list[BrowseFolder]: + """Get a list of tag names as BrowseFolder.""" + tags = await self.radios.tags( + hide_broken=True, + order=Order.STATION_COUNT, + reverse=True, + ) + tags.sort(key=lambda tag: tag.name) + return [ + BrowseFolder( + item_id=tag.name.lower(), + provider=self.domain, + path=base_path + "/" + tag.name.lower(), + name=tag.name, + ) + for tag in tags + ] + + @use_cache(3600 * 24) + async def get_country_folders(self, base_path: str) -> list[BrowseFolder]: + """Get a list of country names as BrowseFolder.""" + items: list[BrowseFolder] = [] + for country in await self.radios.countries(order=Order.NAME, hide_broken=True, limit=1000): + folder = BrowseFolder( + item_id=country.code.lower(), + provider=self.domain, + path=base_path + "/" + country.code.lower(), + name=country.name, + ) + folder.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=country.favicon, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + items.append(folder) + return items + + @use_cache(3600) + async def get_by_popularity(self) -> Sequence[Radio]: + """Get radio stations by popularity.""" + stations = await self.radios.stations( + hide_broken=True, + limit=1000, + order=Order.CLICK_COUNT, + reverse=True, + ) + items = [] + for station in stations: + items.append(await self._parse_radio(station)) + return items + + @use_cache(3600) + async def get_by_tag(self, tag: str) -> Sequence[Radio]: + """Get radio stations by tag.""" + items = [] + stations = await self.radios.stations( + filter_by=FilterBy.TAG_EXACT, + filter_term=tag, + hide_broken=True, + limit=1000, + order=Order.CLICK_COUNT, + reverse=False, + ) + for station in stations: + items.append(await self._parse_radio(station)) + return items + + @use_cache(3600) + async def get_by_country(self, country_code: str) -> list[Radio]: + """Get radio stations by country.""" + items = [] + stations = await self.radios.stations( + filter_by=FilterBy.COUNTRY_CODE_EXACT, + filter_term=country_code, + hide_broken=True, + limit=1000, + order=Order.CLICK_COUNT, + reverse=False, + ) + for station in stations: + items.append(await self._parse_radio(station)) + return items + + async def get_radio(self, prov_radio_id: str) -> Radio: + """Get radio station details.""" + radio = await self.radios.station(uuid=prov_radio_id) + if not radio: + raise MediaNotFoundError(f"Radio station {prov_radio_id} not found") + return await self._parse_radio(radio) + + async def _parse_radio(self, radio_obj: Station) -> Radio: + """Parse Radio object from json obj returned from api.""" + radio = Radio( + item_id=radio_obj.uuid, + provider=self.domain, + name=radio_obj.name, + provider_mappings={ + ProviderMapping( + item_id=radio_obj.uuid, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + radio.metadata.popularity = radio_obj.votes + radio.metadata.links = {MediaItemLink(type=LinkType.WEBSITE, url=radio_obj.homepage)} + radio.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=radio_obj.favicon, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + + return radio + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a radio station.""" + stream = await self.radios.station(uuid=item_id) + if not stream: + raise MediaNotFoundError(f"Radio station {item_id} not found") + await self.radios.station_click(uuid=item_id) + return StreamDetails( + provider=self.domain, + item_id=item_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(stream.codec), + ), + media_type=MediaType.RADIO, + stream_type=StreamType.HTTP, + path=stream.url_resolved, + can_seek=False, + ) diff --git a/music_assistant/providers/radiobrowser/manifest.json b/music_assistant/providers/radiobrowser/manifest.json new file mode 100644 index 00000000..263a29d5 --- /dev/null +++ b/music_assistant/providers/radiobrowser/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "music", + "domain": "radiobrowser", + "name": "RadioBrowser", + "description": "Search radio streams from RadioBrowser in Music Assistant.", + "codeowners": ["@gieljnssns"], + "requirements": ["radios==0.3.2"], + "documentation": "https://music-assistant.io/music-providers/radio-browser/", + "multi_instance": false, + "icon": "radio" +} diff --git a/music_assistant/providers/siriusxm/__init__.py b/music_assistant/providers/siriusxm/__init__.py new file mode 100644 index 00000000..f362c9a6 --- /dev/null +++ b/music_assistant/providers/siriusxm/__init__.py @@ -0,0 +1,327 @@ +"""SiriusXM Music Provider for Music Assistant.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator, Awaitable, Sequence +from typing import TYPE_CHECKING, Any, cast + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + LinkType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( + AudioFormat, + ImageType, + ItemMapping, + MediaItemImage, + MediaItemLink, + MediaItemType, + ProviderMapping, + Radio, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.helpers.util import select_free_port +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +import sxm.http +from sxm import SXMClientAsync +from sxm.models import QualitySize, RegionChoice, XMChannel, XMLiveChannel + +CONF_SXM_USERNAME = "sxm_email_address" +CONF_SXM_PASSWORD = "sxm_password" +CONF_SXM_REGION = "sxm_region" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return SiriusXMProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_SXM_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + ), + ConfigEntry( + key=CONF_SXM_PASSWORD, + type=ConfigEntryType.SECURE_STRING, + label="Password", + required=True, + ), + ConfigEntry( + key=CONF_SXM_REGION, + type=ConfigEntryType.STRING, + default_value="US", + options=( + ConfigValueOption(title="United States", value="US"), + ConfigValueOption(title="Canada", value="CA"), + ), + label="Region", + required=True, + ), + ) + + +class SiriusXMProvider(MusicProvider): + """SiriusXM Music Provider.""" + + _username: str + _password: str + _region: str + _client: SXMClientAsync + + _channels: list[XMChannel] + + _sxm_server: Webserver + _base_url: str + + _current_stream_details: StreamDetails | None = None + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return ( + ProviderFeature.BROWSE, + ProviderFeature.LIBRARY_RADIOS, + ) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + username: str = self.config.get_value(CONF_SXM_USERNAME) + password: str = self.config.get_value(CONF_SXM_PASSWORD) + + region: RegionChoice = ( + RegionChoice.US if self.config.get_value(CONF_SXM_REGION) == "US" else RegionChoice.CA + ) + + self._client = SXMClientAsync( + username, + password, + region, + quality=QualitySize.LARGE_256k, + update_handler=self._channel_updated, + ) + + self.logger.info("Authenticating with SiriusXM") + if not await self._client.authenticate(): + raise LoginFailed("Could not login to SiriusXM") + + self.logger.info("Successfully authenticated") + + await self._refresh_channels() + + # Set up the sxm server for streaming + bind_ip = "127.0.0.1" + bind_port = await select_free_port(8100, 9999) + + self._base_url = f"{bind_ip}:{bind_port}" + http_handler = sxm.http.make_http_handler(self._client) + + self._sxm_server = Webserver(self.logger) + + await self._sxm_server.setup( + bind_ip=bind_ip, + bind_port=bind_port, + base_url=self._base_url, + static_routes=[ + ("*", "/{tail:.*}", cast(Awaitable, http_handler)), + ], + ) + + self.logger.debug(f"SXM Proxy server running at {bind_ip}:{bind_port}") + + async def unload(self) -> None: + """ + Handle unload/close of the provider. + + Called when provider is deregistered (e.g. MA exiting or config reloading). + """ + await self._sxm_server.close() + + @property + def is_streaming_provider(self) -> bool: + """ + Return True if the provider is a streaming provider. + + This literally means that the catalog is not the same as the library contents. + For local based providers (files, plex), the catalog is the same as the library content. + It also means that data is if this provider is NOT a streaming provider, + data cross instances is unique, the catalog and library differs per instance. + + Setting this to True will only query one instance of the provider for search and lookups. + Setting this to False will query all instances of this provider for search and lookups. + """ + return True + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + for channel in self._channels_by_id.values(): + if channel.is_favorite: + yield self._parse_radio(channel) + + async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] + """Get full radio details by id.""" + if prov_radio_id not in self._channels_by_id: + raise MediaNotFoundError("Station not found") + + return self._parse_radio(self._channels_by_id[prov_radio_id]) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track/radio.""" + hls_path = f"http://{self._base_url}/{item_id}.m3u8" + + # Keep a reference to the current `StreamDetails` object so that we can + # update the `stream_title` attribute as callbacks come in from the + # sxm-client with the channel's live data. + # See `_channel_updated` for where this is handled. + self._current_stream_details = StreamDetails( + item_id=item_id, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.AAC, + ), + stream_type=StreamType.HLS, + media_type=MediaType.RADIO, + path=hls_path, + can_seek=False, + ) + + return self._current_stream_details + + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: + """Browse this provider's items. + + :param path: The path to browse, (e.g. provider_id://artists). + """ + return [self._parse_radio(channel) for channel in self._channels] + + def _channel_updated(self, live_channel_raw: dict[str, Any]) -> None: + """Handle a channel update event.""" + live_data = XMLiveChannel.from_dict(live_channel_raw) + + self.logger.debug(f"Got update for SiriusXM channel {live_data.id}") + current_channel = self._current_stream_details.item_id + + if live_data.id != current_channel: + # This can happen when changing channels + self.logger.debug( + f"Received update for channel {live_data.id}, current channel is {current_channel}" + ) + return + + latest_cut_marker = live_data.get_latest_cut() + + if latest_cut_marker: + latest_cut = latest_cut_marker.cut + title = latest_cut.title + artists = ", ".join([a.name for a in latest_cut.artists]) + + self._current_stream_details.stream_title = f"{title} - {artists}" + + async def _refresh_channels(self) -> bool: + self._channels = await self._client.channels + + self._channels_by_id = {} + + for channel in self._channels: + self._channels_by_id[channel.id] = channel + + return True + + def _parse_radio(self, channel: XMChannel) -> Radio: + radio = Radio( + provider=self.instance_id, + item_id=channel.id, + name=channel.name, + provider_mappings={ + ProviderMapping( + provider_domain=self.domain, + provider_instance=self.instance_id, + item_id=channel.id, + ) + }, + ) + + icon = next((i.url for i in channel.images if i.width == 300 and i.height == 300), None) + banner = next( + (i.url for i in channel.images if i.name in ("channel hero image", "background")), None + ) + + images: list[MediaItemImage] = [] + + if icon is not None: + images.append( + MediaItemImage( + provider=self.instance_id, + type=ImageType.THUMB, + path=icon, + remotely_accessible=True, + ) + ) + images.append( + MediaItemImage( + provider=self.instance_id, + type=ImageType.LOGO, + path=icon, + remotely_accessible=True, + ) + ) + + if banner is not None: + images.append( + MediaItemImage( + provider=self.instance_id, + type=ImageType.BANNER, + path=banner, + remotely_accessible=True, + ) + ) + images.append( + MediaItemImage( + provider=self.instance_id, + type=ImageType.LANDSCAPE, + path=banner, + remotely_accessible=True, + ) + ) + + radio.metadata.images = images + radio.metadata.links = [MediaItemLink(type=LinkType.WEBSITE, url=channel.url)] + radio.metadata.description = channel.medium_description + radio.metadata.explicit = bool(channel.is_mature) + radio.metadata.genres = [cat.name for cat in channel.categories] + + return radio diff --git a/music_assistant/providers/siriusxm/icon.svg b/music_assistant/providers/siriusxm/icon.svg new file mode 100644 index 00000000..bb26dc74 --- /dev/null +++ b/music_assistant/providers/siriusxm/icon.svg @@ -0,0 +1,7 @@ + + SIRI_BIG copy-svg + + + diff --git a/music_assistant/providers/siriusxm/icon_dark.svg b/music_assistant/providers/siriusxm/icon_dark.svg new file mode 100644 index 00000000..fe3bcf3b --- /dev/null +++ b/music_assistant/providers/siriusxm/icon_dark.svg @@ -0,0 +1,7 @@ + + SIRI_BIG copy-svg + + + diff --git a/music_assistant/providers/siriusxm/manifest.json b/music_assistant/providers/siriusxm/manifest.json new file mode 100644 index 00000000..d1a36b58 --- /dev/null +++ b/music_assistant/providers/siriusxm/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "siriusxm", + "name": "SiriusXM", + "description": "Support for the SiriusXM streaming radio provider in Music Assistant.", + "codeowners": ["@btoconnor"], + "requirements": ["sxm==0.2.8"], + "documentation": "https://music-assistant.io/music-providers/siriusxm/", + "multi_instance": false +} diff --git a/music_assistant/providers/slimproto/__init__.py b/music_assistant/providers/slimproto/__init__.py new file mode 100644 index 00000000..fc9aaffb --- /dev/null +++ b/music_assistant/providers/slimproto/__init__.py @@ -0,0 +1,974 @@ +"""Base/builtin provider with support for players using slimproto.""" + +from __future__ import annotations + +import asyncio +import logging +import statistics +import time +from collections import deque +from collections.abc import Iterator +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from aiohttp import web +from aioslimproto.client import PlayerState as SlimPlayerState +from aioslimproto.client import SlimClient +from aioslimproto.client import TransitionType as SlimTransition +from aioslimproto.models import EventType as SlimEventType +from aioslimproto.models import Preset as SlimPreset +from aioslimproto.models import VisualisationType as SlimVisualisationType +from aioslimproto.server import SlimServer +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_EQ_BASS, + CONF_ENTRY_EQ_MID, + CONF_ENTRY_EQ_TREBLE, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, + CONF_ENTRY_OUTPUT_CHANNELS, + CONF_ENTRY_SYNC_ADJUST, + ConfigEntry, + ConfigValueOption, + ConfigValueType, + PlayerConfig, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + MediaType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderFeature, + RepeatMode, +) +from music_assistant_models.errors import MusicAssistantError, SetupFailedError +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.constants import ( + CONF_CROSSFADE, + CONF_CROSSFADE_DURATION, + CONF_ENFORCE_MP3, + CONF_PORT, + CONF_SYNC_ADJUST, + VERBOSE_LOG_LEVEL, +) +from music_assistant.helpers.audio import get_ffmpeg_stream, get_player_filter_params +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.player_group import PlayerGroupProvider + +from .multi_client_stream import MultiClientStream + +if TYPE_CHECKING: + from aioslimproto.models import SlimEvent + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +CACHE_KEY_PREV_STATE = "slimproto_prev_state" + + +STATE_MAP = { + SlimPlayerState.BUFFERING: PlayerState.PLAYING, + SlimPlayerState.BUFFER_READY: PlayerState.PLAYING, + SlimPlayerState.PAUSED: PlayerState.PAUSED, + SlimPlayerState.PLAYING: PlayerState.PLAYING, + SlimPlayerState.STOPPED: PlayerState.IDLE, +} +REPEATMODE_MAP = {RepeatMode.OFF: 0, RepeatMode.ONE: 1, RepeatMode.ALL: 2} + +# sync constants +MIN_DEVIATION_ADJUST = 8 # 5 milliseconds +MIN_REQ_PLAYPOINTS = 8 # we need at least 8 measurements +DEVIATION_JUMP_IGNORE = 500 # ignore a sudden unrealistic jump +MAX_SKIP_AHEAD_MS = 800 # 0.8 seconds + + +@dataclass +class SyncPlayPoint: + """Simple structure to describe a Sync Playpoint.""" + + timestamp: float + sync_master: str + diff: int + + +CONF_CLI_TELNET_PORT = "cli_telnet_port" +CONF_CLI_JSON_PORT = "cli_json_port" +CONF_DISCOVERY = "discovery" +CONF_DISPLAY = "display" +CONF_VISUALIZATION = "visualization" + +DEFAULT_PLAYER_VOLUME = 20 +DEFAULT_SLIMPROTO_PORT = 3483 +DEFAULT_VISUALIZATION = SlimVisualisationType.NONE + + +CONF_ENTRY_DISPLAY = ConfigEntry( + key=CONF_DISPLAY, + type=ConfigEntryType.BOOLEAN, + default_value=False, + required=False, + label="Enable display support", + description="Enable/disable native display support on squeezebox or squeezelite32 hardware.", + category="advanced", +) +CONF_ENTRY_VISUALIZATION = ConfigEntry( + key=CONF_VISUALIZATION, + type=ConfigEntryType.STRING, + default_value=DEFAULT_VISUALIZATION, + options=tuple( + ConfigValueOption(title=x.name.replace("_", " ").title(), value=x.value) + for x in SlimVisualisationType + ), + required=False, + label="Visualization type", + description="The type of visualization to show on the display " + "during playback if the device supports this.", + category="advanced", + depends_on=CONF_DISPLAY, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return SlimprotoProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_CLI_TELNET_PORT, + type=ConfigEntryType.INTEGER, + default_value=9090, + label="Classic Squeezebox CLI Port", + description="Some slimproto based players require the presence of the telnet CLI " + " to request more information. \n\n" + "By default this CLI is hosted on port 9090 but some players also accept " + "a different port. Set to 0 to disable this functionality.\n\n" + "Commands allowed on this interface are very limited and just enough to satisfy " + "player compatibility, so security risks are minimized to practically zero." + "You may safely disable this option if you have no players that rely on this feature " + "or you dont care about the additional metadata.", + category="advanced", + ), + ConfigEntry( + key=CONF_CLI_JSON_PORT, + type=ConfigEntryType.INTEGER, + default_value=9000, + label="JSON-RPC CLI/API Port", + description="Some slimproto based players require the presence of the JSON-RPC " + "API from LMS to request more information. For example to fetch the album cover " + "and other metadata. \n\n" + "This JSON-RPC API is compatible with Logitech Media Server but not all commands " + "are implemented. Just enough to satisfy player compatibility. \n\n" + "By default this JSON CLI is hosted on port 9000 but most players also accept " + "it on a different port. Set to 0 to disable this functionality.\n\n" + "You may safely disable this option if you have no players that rely on this feature " + "or you dont care about the additional metadata.", + category="advanced", + ), + ConfigEntry( + key=CONF_DISCOVERY, + type=ConfigEntryType.BOOLEAN, + default_value=True, + label="Enable Discovery server", + description="Broadcast discovery packets for slimproto clients to automatically " + "discover and connect to this server. \n\n" + "You may want to disable this feature if you are running multiple slimproto servers " + "on your network and/or you don't want clients to auto connect to this server.", + category="advanced", + ), + ConfigEntry( + key=CONF_PORT, + type=ConfigEntryType.INTEGER, + default_value=DEFAULT_SLIMPROTO_PORT, + label="Slimproto port", + description="The TCP/UDP port to run the slimproto sockets server. " + "The default is 3483 and using a different port is not supported by " + "hardware squeezebox players. Only adjust this port if you want to " + "use other slimproto based servers side by side with (squeezelite) software players.", + category="advanced", + ), + ) + + +class SlimprotoProvider(PlayerProvider): + """Base/builtin provider for players using the SLIM protocol (aka slimproto).""" + + slimproto: SlimServer + _sync_playpoints: dict[str, deque[SyncPlayPoint]] + _do_not_resync_before: dict[str, float] + _multi_streams: dict[str, MultiClientStream] + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self._sync_playpoints = {} + self._do_not_resync_before = {} + self._multi_streams = {} + control_port = self.config.get_value(CONF_PORT) + telnet_port = self.config.get_value(CONF_CLI_TELNET_PORT) + json_port = self.config.get_value(CONF_CLI_JSON_PORT) + # silence aioslimproto logger a bit + if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("aioslimproto").setLevel(logging.DEBUG) + else: + logging.getLogger("aioslimproto").setLevel(self.logger.level + 10) + self.slimproto = SlimServer( + cli_port=telnet_port or None, + cli_port_json=json_port or None, + ip_address=self.mass.streams.publish_ip, + name="Music Assistant", + control_port=control_port, + ) + # start slimproto socket server + try: + await self.slimproto.start() + except OSError as err: + raise SetupFailedError( + "Unable to start the Slimproto server - " + "is one of the required TCP ports already taken ?" + ) from err + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + await super().loaded_in_mass() + self.slimproto.subscribe(self._client_callback) + self.mass.streams.register_dynamic_route( + "/slimproto/multi", self._serve_multi_client_stream + ) + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + self.mass.streams.unregister_dynamic_route("/slimproto/multi") + await self.slimproto.stop() + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + base_entries = await super().get_player_config_entries(player_id) + if slimclient := self.slimproto.get_player(player_id): + max_sample_rate = int(slimclient.max_sample_rate) + else: + # player not (yet) connected? use default + max_sample_rate = 48000 + # create preset entries (for players that support it) + preset_entries = () + presets = [] + async for playlist in self.mass.music.playlists.iter_library_items(True): + presets.append(ConfigValueOption(playlist.name, playlist.uri)) + async for radio in self.mass.music.radio.iter_library_items(True): + presets.append(ConfigValueOption(radio.name, radio.uri)) + preset_count = 10 + preset_entries = tuple( + ConfigEntry( + key=f"preset_{index}", + type=ConfigEntryType.STRING, + options=presets, + label=f"Preset {index}", + description="Assign a playable item to the player's preset. " + "Only supported on real squeezebox hardware or jive(lite) based emulators.", + category="presets", + required=False, + ) + for index in range(1, preset_count + 1) + ) + return ( + base_entries + + preset_entries + + ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_EQ_BASS, + CONF_ENTRY_EQ_MID, + CONF_ENTRY_EQ_TREBLE, + CONF_ENTRY_OUTPUT_CHANNELS, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_SYNC_ADJUST, + CONF_ENTRY_DISPLAY, + CONF_ENTRY_VISUALIZATION, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, + create_sample_rates_config_entry(max_sample_rate, 24, 48000, 24), + ) + ) + + async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: + """Call (by config manager) when the configuration of a player changes.""" + if slimplayer := self.slimproto.get_player(config.player_id): + await self._set_preset_items(slimplayer) + await self._set_display(slimplayer) + await super().on_player_config_change(config, changed_keys) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + # forward command to player and any connected sync members + async with TaskManager(self.mass) as tg: + for slimplayer in self._get_sync_clients(player_id): + tg.create_task(slimplayer.stop()) + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + # forward command to player and any connected sync members + async with TaskManager(self.mass) as tg: + for slimplayer in self._get_sync_clients(player_id): + tg.create_task(slimplayer.play()) + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + player = self.mass.players.get(player_id) + if player.synced_to: + msg = "A synced player cannot receive play commands directly" + raise RuntimeError(msg) + + if not player.group_childs: + slimplayer = self.slimproto.get_player(player_id) + # simple, single-player playback + await self._handle_play_url( + slimplayer, + url=media.uri, + media=media, + send_flush=True, + auto_play=False, + ) + return + + # this is a syncgroup, we need to handle this with a multi client stream + master_audio_format = AudioFormat( + content_type=ContentType.PCM_F32LE, + sample_rate=48000, + bit_depth=32, + ) + if media.media_type == MediaType.ANNOUNCEMENT: + # special case: stream announcement + audio_source = self.mass.streams.get_announcement_stream( + media.custom_data["url"], + output_format=master_audio_format, + use_pre_announce=media.custom_data["use_pre_announce"], + ) + elif media.queue_id.startswith("ugp_"): + # special case: UGP stream + ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") + ugp_stream = ugp_provider.ugp_streams[media.queue_id] + audio_source = ugp_stream.subscribe() + elif media.queue_id and media.queue_item_id: + # regular queue stream request + audio_source = self.mass.streams.get_flow_stream( + queue=self.mass.player_queues.get(media.queue_id), + start_queue_item=self.mass.player_queues.get_item( + media.queue_id, media.queue_item_id + ), + pcm_format=master_audio_format, + ) + else: + # assume url or some other direct path + # NOTE: this will fail if its an uri not playable by ffmpeg + audio_source = get_ffmpeg_stream( + audio_input=media.uri, + input_format=AudioFormat(ContentType.try_parse(media.uri)), + output_format=master_audio_format, + ) + # start the stream task + self._multi_streams[player_id] = stream = MultiClientStream( + audio_source=audio_source, audio_format=master_audio_format + ) + base_url = f"{self.mass.streams.base_url}/slimproto/multi?player_id={player_id}&fmt=flac" + + # forward to downstream play_media commands + async with TaskManager(self.mass) as tg: + for slimplayer in self._get_sync_clients(player_id): + url = f"{base_url}&child_player_id={slimplayer.player_id}" + if self.mass.config.get_raw_player_config_value( + slimplayer.player_id, CONF_ENFORCE_MP3, False + ): + url = url.replace("flac", "mp3") + stream.expected_clients += 1 + tg.create_task( + self._handle_play_url( + slimplayer, + url=url, + media=media, + send_flush=True, + auto_play=False, + ) + ) + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next queue item on the player.""" + if not (slimplayer := self.slimproto.get_player(player_id)): + return + url = media.uri + if self.mass.config.get_raw_player_config_value( + slimplayer.player_id, CONF_ENFORCE_MP3, False + ): + url = url.replace("flac", "mp3") + + await self._handle_play_url( + slimplayer, + url=url, + media=media, + enqueue=True, + send_flush=False, + auto_play=True, + ) + + async def _handle_play_url( + self, + slimplayer: SlimClient, + url: str, + media: PlayerMedia, + enqueue: bool = False, + send_flush: bool = True, + auto_play: bool = False, + ) -> None: + """Handle playback of an url on slimproto player(s).""" + player_id = slimplayer.player_id + if crossfade := await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE): + transition_duration = await self.mass.config.get_player_config_value( + player_id, CONF_CROSSFADE_DURATION + ) + else: + transition_duration = 0 + + metadata = { + "item_id": media.uri, + "title": media.title, + "album": media.album, + "artist": media.artist, + "image_url": media.image_url, + "duration": media.duration, + "queue_id": media.queue_id, + "queue_item_id": media.queue_item_id, + } + queue = self.mass.player_queues.get(media.queue_id or player_id) + slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] + slimplayer.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) + await slimplayer.play_url( + url=url, + mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", + metadata=metadata, + enqueue=enqueue, + send_flush=send_flush, + transition=SlimTransition.CROSSFADE if crossfade else SlimTransition.NONE, + transition_duration=transition_duration, + # if autoplay=False playback will not start automatically + # instead 'buffer ready' will be called when the buffer is full + # to coordinate a start of multiple synced players + autostart=auto_play, + ) + # if queue is set to single track repeat, + # immediately set this track as the next + # this prevents race conditions with super short audio clips (on single repeat) + # https://github.com/music-assistant/hass-music-assistant/issues/2059 + if queue.repeat_mode == RepeatMode.ONE: + self.mass.call_later( + 0.2, + slimplayer.play_url( + url=url, + mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", + metadata=metadata, + enqueue=True, + send_flush=False, + transition=SlimTransition.CROSSFADE if crossfade else SlimTransition.NONE, + transition_duration=transition_duration, + autostart=True, + ), + ) + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + # forward command to player and any connected sync members + async with TaskManager(self.mass) as tg: + for slimplayer in self._get_sync_clients(player_id): + tg.create_task(slimplayer.pause()) + + async def cmd_power(self, player_id: str, powered: bool) -> None: + """Send POWER command to given player.""" + if slimplayer := self.slimproto.get_player(player_id): + await slimplayer.power(powered) + # store last state in cache + await self.mass.cache.set( + player_id, (powered, slimplayer.volume_level), base_key=CACHE_KEY_PREV_STATE + ) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + if slimplayer := self.slimproto.get_player(player_id): + await slimplayer.volume_set(volume_level) + # store last state in cache + await self.mass.cache.set( + player_id, (slimplayer.powered, volume_level), base_key=CACHE_KEY_PREV_STATE + ) + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + if slimplayer := self.slimproto.get_player(player_id): + await slimplayer.mute(muted) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player.""" + child_player = self.mass.players.get(player_id) + assert child_player # guard + parent_player = self.mass.players.get(target_player) + assert parent_player # guard + if parent_player.synced_to: + raise RuntimeError("Parent player is already synced!") + if child_player.synced_to and child_player.synced_to != target_player: + raise RuntimeError("Player is already synced to another player") + # always make sure that the parent player is part of the sync group + parent_player.group_childs.add(parent_player.player_id) + parent_player.group_childs.add(child_player.player_id) + child_player.synced_to = parent_player.player_id + # check if we should (re)start or join a stream session + # TODO: support late joining of a client into an existing stream session + # so it doesn't need to be restarted anymore. + active_queue = self.mass.player_queues.get_active_queue(parent_player.player_id) + if active_queue.state == PlayerState.PLAYING: + # playback needs to be restarted to form a new multi client stream session + # this could potentially be called by multiple players at the exact same time + # so we debounce the resync a bit here with a timer + self.mass.call_later( + 1, + self.mass.player_queues.resume, + active_queue.queue_id, + fade_in=False, + task_id=f"resume_{active_queue.queue_id}", + ) + else: + # make sure that the player manager gets an update + self.mass.players.update(child_player.player_id, skip_forward=True) + self.mass.players.update(parent_player.player_id, skip_forward=True) + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + player = self.mass.players.get(player_id, raise_unavailable=True) + if player.synced_to: + group_leader = self.mass.players.get(player.synced_to, raise_unavailable=True) + if player_id in group_leader.group_childs: + group_leader.group_childs.remove(player_id) + player.synced_to = None + if slimclient := self.slimproto.get_player(player_id): + await slimclient.stop() + # make sure that the player manager gets an update + self.mass.players.update(player.player_id, skip_forward=True) + self.mass.players.update(group_leader.player_id, skip_forward=True) + + def _client_callback( + self, + event: SlimEvent, + ) -> None: + if self.mass.closing: + return + + if event.type == SlimEventType.PLAYER_DISCONNECTED: + if mass_player := self.mass.players.get(event.player_id): + mass_player.available = False + self.mass.players.update(mass_player.player_id) + return + + if not (slimplayer := self.slimproto.get_player(event.player_id)): + return + + if event.type == SlimEventType.PLAYER_CONNECTED: + self.mass.create_task(self._handle_connected(slimplayer)) + return + + if event.type == SlimEventType.PLAYER_BUFFER_READY: + self.mass.create_task(self._handle_buffer_ready(slimplayer)) + return + + if event.type == SlimEventType.PLAYER_HEARTBEAT: + self._handle_player_heartbeat(slimplayer) + return + + if event.type in (SlimEventType.PLAYER_BTN_EVENT, SlimEventType.PLAYER_CLI_EVENT): + self.mass.create_task(self._handle_player_cli_event(slimplayer, event)) + return + + # forward player update to MA player controller + self.mass.create_task(self._handle_player_update(slimplayer)) + + async def _handle_player_update(self, slimplayer: SlimClient) -> None: + """Process SlimClient update/add to Player controller.""" + player_id = slimplayer.player_id + player = self.mass.players.get(player_id, raise_unavailable=False) + if not player: + # player does not yet exist, create it + player = Player( + player_id=player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=slimplayer.name, + available=True, + powered=slimplayer.powered, + device_info=DeviceInfo( + model=slimplayer.device_model, + address=slimplayer.device_address, + manufacturer=slimplayer.device_type, + ), + supported_features=( + PlayerFeature.POWER, + PlayerFeature.SYNC, + PlayerFeature.VOLUME_SET, + PlayerFeature.PAUSE, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.ENQUEUE, + ), + ) + await self.mass.players.register_or_update(player) + + # update player state on player events + player.available = True + if slimplayer.current_media and (metadata := slimplayer.current_media.metadata): + player.current_media = PlayerMedia( + uri=metadata.get("item_id"), + title=metadata.get("title"), + album=metadata.get("album"), + artist=metadata.get("artist"), + image_url=metadata.get("image_url"), + duration=metadata.get("duration"), + queue_id=metadata.get("queue_id"), + queue_item_id=metadata.get("queue_item_id"), + ) + else: + player.current_media = None + player.active_source = player.player_id + player.name = slimplayer.name + player.powered = slimplayer.powered + player.state = STATE_MAP[slimplayer.state] + player.volume_level = slimplayer.volume_level + player.volume_muted = slimplayer.muted + self.mass.players.update(player_id) + + def _handle_player_heartbeat(self, slimplayer: SlimClient) -> None: + """Process SlimClient elapsed_time update.""" + if slimplayer.state == SlimPlayerState.STOPPED: + # ignore server heartbeats when stopped + return + + # elapsed time change on the player will be auto picked up + # by the player manager. + if not (player := self.mass.players.get(slimplayer.player_id)): + # race condition?! + return + player.elapsed_time = slimplayer.elapsed_seconds + player.elapsed_time_last_updated = time.time() + + # handle sync + if player.synced_to: + self._handle_client_sync(slimplayer) + + async def _handle_player_cli_event(self, slimplayer: SlimClient, event: SlimEvent) -> None: + """Process CLI Event.""" + if not event.data: + return + queue = self.mass.player_queues.get_active_queue(slimplayer.player_id) + if event.data.startswith("button preset_") and event.data.endswith(".single"): + preset_id = event.data.split("preset_")[1].split(".")[0] + preset_index = int(preset_id) - 1 + if len(slimplayer.presets) >= preset_index + 1: + preset = slimplayer.presets[preset_index] + await self.mass.player_queues.play_media(queue.queue_id, preset.uri) + elif event.data == "button repeat": + if queue.repeat_mode == RepeatMode.OFF: + repeat_mode = RepeatMode.ONE + elif queue.repeat_mode == RepeatMode.ONE: + repeat_mode = RepeatMode.ALL + else: + repeat_mode = RepeatMode.OFF + self.mass.player_queues.set_repeat(queue.queue_id, repeat_mode) + slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] + slimplayer.signal_update() + elif event.data == "button shuffle": + self.mass.player_queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled) + slimplayer.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) + slimplayer.signal_update() + elif event.data in ("button jump_fwd", "button fwd"): + await self.mass.player_queues.next(queue.queue_id) + elif event.data in ("button jump_rew", "button rew"): + await self.mass.player_queues.previous(queue.queue_id) + elif event.data.startswith("time "): + # seek request + _, param = event.data.split(" ", 1) + if param.isnumeric(): + await self.mass.player_queues.seek(queue.queue_id, int(param)) + self.logger.debug("CLI Event: %s", event.data) + + def _handle_client_sync(self, slimplayer: SlimClient) -> None: + """Synchronize audio of a sync slimplayer.""" + player = self.mass.players.get(slimplayer.player_id) + sync_master_id = player.synced_to + if not sync_master_id: + # we only correct sync members, not the sync master itself + return + if not (sync_master := self.slimproto.get_player(sync_master_id)): + return # just here as a guard as bad things can happen + + if sync_master.state != SlimPlayerState.PLAYING: + return + if slimplayer.state != SlimPlayerState.PLAYING: + return + if slimplayer.player_id not in self._sync_playpoints: + return + + # we collect a few playpoints of the player to determine + # average lag/drift so we can adjust accordingly + sync_playpoints = self._sync_playpoints[slimplayer.player_id] + + now = time.time() + if now < self._do_not_resync_before[slimplayer.player_id]: + return + + last_playpoint = sync_playpoints[-1] if sync_playpoints else None + if last_playpoint and (now - last_playpoint.timestamp) > 10: + # last playpoint is too old, invalidate + sync_playpoints.clear() + if last_playpoint and last_playpoint.sync_master != sync_master.player_id: + # this should not happen, but just in case + sync_playpoints.clear() + + diff = int( + self._get_corrected_elapsed_milliseconds(sync_master) + - self._get_corrected_elapsed_milliseconds(slimplayer) + ) + + # ignore unexpected spikes + if ( + sync_playpoints + and abs(statistics.fmean(x.diff for x in sync_playpoints)) > DEVIATION_JUMP_IGNORE + ): + return + + # we can now append the current playpoint to our list + sync_playpoints.append(SyncPlayPoint(now, sync_master.player_id, diff)) + + min_req_playpoints = 2 if sync_master.elapsed_seconds < 2 else MIN_REQ_PLAYPOINTS + if len(sync_playpoints) < min_req_playpoints: + return + + # get the average diff + avg_diff = statistics.fmean(x.diff for x in sync_playpoints) + delta = int(abs(avg_diff)) + + if delta < MIN_DEVIATION_ADJUST: + return + + # resync the player by skipping ahead or pause for x amount of (milli)seconds + sync_playpoints.clear() + self._do_not_resync_before[player.player_id] = now + 5 + if avg_diff > MAX_SKIP_AHEAD_MS: + # player lagging behind more than MAX_SKIP_AHEAD_MS, + # we need to correct the sync_master + self.logger.debug("%s resync: pauseFor %sms", sync_master.name, delta) + self.mass.create_task(sync_master.pause_for(delta)) + elif avg_diff > 0: + # handle player lagging behind, fix with skip_ahead + self.logger.debug("%s resync: skipAhead %sms", player.display_name, delta) + self.mass.create_task(slimplayer.skip_over(delta)) + else: + # handle player is drifting too far ahead, use pause_for to adjust + self.logger.debug("%s resync: pauseFor %sms", player.display_name, delta) + self.mass.create_task(slimplayer.pause_for(delta)) + + async def _handle_buffer_ready(self, slimplayer: SlimClient) -> None: + """Handle buffer ready event, player has buffered a (new) track. + + Only used when autoplay=0 for coordinated start of synced players. + """ + player = self.mass.players.get(slimplayer.player_id) + if player.synced_to: + # unpause of sync child is handled by sync master + return + if not player.group_childs: + # not a sync group, continue + await slimplayer.unpause_at(slimplayer.jiffies) + return + count = 0 + while count < 40: + childs_total = 0 + childs_ready = 0 + await asyncio.sleep(0.2) + for sync_child in self._get_sync_clients(player.player_id): + childs_total += 1 + if sync_child.state == SlimPlayerState.BUFFER_READY: + childs_ready += 1 + if childs_total == childs_ready: + break + + # all child's ready (or timeout) - start play + async with TaskManager(self.mass) as tg: + for _client in self._get_sync_clients(player.player_id): + self._sync_playpoints.setdefault( + _client.player_id, deque(maxlen=MIN_REQ_PLAYPOINTS) + ).clear() + # NOTE: Officially you should do an unpause_at based on the player timestamp + # but I did not have any good results with that. + # Instead just start playback on all players and let the sync logic work out + # the delays etc. + self._do_not_resync_before[_client.player_id] = time.time() + 1 + tg.create_task(_client.pause_for(200)) + + async def _handle_connected(self, slimplayer: SlimClient) -> None: + """Handle a slimplayer connected event.""" + player_id = slimplayer.player_id + self.logger.info("Player %s connected", slimplayer.name or player_id) + # set presets and display + await self._set_preset_items(slimplayer) + await self._set_display(slimplayer) + # update all attributes + await self._handle_player_update(slimplayer) + # restore volume and power state + if last_state := await self.mass.cache.get(player_id, base_key=CACHE_KEY_PREV_STATE): + init_power = last_state[0] + init_volume = last_state[1] + else: + init_volume = DEFAULT_PLAYER_VOLUME + init_power = False + await slimplayer.power(init_power) + await slimplayer.stop() + await slimplayer.volume_set(init_volume) + + def _get_sync_clients(self, player_id: str) -> Iterator[SlimClient]: + """Get all sync clients for a player.""" + player = self.mass.players.get(player_id) + # we need to return the player itself too + group_child_ids = {player_id} + group_child_ids.update(player.group_childs) + for child_id in group_child_ids: + if slimplayer := self.slimproto.get_player(child_id): + yield slimplayer + + def _get_corrected_elapsed_milliseconds(self, slimplayer: SlimClient) -> int: + """Return corrected elapsed milliseconds.""" + sync_delay = self.mass.config.get_raw_player_config_value( + slimplayer.player_id, CONF_SYNC_ADJUST, 0 + ) + return slimplayer.elapsed_milliseconds - sync_delay + + async def _set_preset_items(self, slimplayer: SlimClient) -> None: + """Set the presets for a player.""" + preset_items: list[SlimPreset] = [] + for preset_index in range(1, 11): + if preset_conf := self.mass.config.get_raw_player_config_value( + slimplayer.player_id, f"preset_{preset_index}" + ): + try: + media_item = await self.mass.music.get_item_by_uri(preset_conf) + preset_items.append( + SlimPreset( + uri=media_item.uri, + text=media_item.name, + icon=self.mass.metadata.get_image_url(media_item.image), + ) + ) + except MusicAssistantError: + # non-existing media item or some other edge case + preset_items.append( + SlimPreset( + uri=f"preset_{preset_index}", + text=f"ERROR ", + icon="", + ) + ) + else: + break + slimplayer.presets = preset_items + + async def _set_display(self, slimplayer: SlimClient) -> None: + """Set the display config for a player.""" + display_enabled = self.mass.config.get_raw_player_config_value( + slimplayer.player_id, + CONF_ENTRY_DISPLAY.key, + CONF_ENTRY_DISPLAY.default_value, + ) + visualization = self.mass.config.get_raw_player_config_value( + slimplayer.player_id, + CONF_ENTRY_VISUALIZATION.key, + CONF_ENTRY_VISUALIZATION.default_value, + ) + await slimplayer.configure_display( + visualisation=SlimVisualisationType(visualization), disabled=not display_enabled + ) + + async def _serve_multi_client_stream(self, request: web.Request) -> web.Response: + """Serve the multi-client flow stream audio to a player.""" + player_id = request.query.get("player_id") + fmt = request.query.get("fmt") + child_player_id = request.query.get("child_player_id") + + if not self.mass.players.get(player_id): + raise web.HTTPNotFound(reason=f"Unknown player: {player_id}") + + if not (child_player := self.mass.players.get(child_player_id)): + raise web.HTTPNotFound(reason=f"Unknown player: {child_player_id}") + + if not (stream := self._multi_streams.get(player_id, None)) or stream.done: + raise web.HTTPNotFound(f"There is no active stream for {player_id}!") + + resp = web.StreamResponse( + status=200, + reason="OK", + headers={ + "Content-Type": f"audio/{fmt}", + }, + ) + await resp.prepare(request) + + # return early if this is not a GET request + if request.method != "GET": + return resp + + # all checks passed, start streaming! + self.logger.debug( + "Start serving multi-client flow audio stream to %s", + child_player.display_name, + ) + + async for chunk in stream.get_stream( + output_format=AudioFormat(content_type=ContentType.try_parse(fmt)), + filter_params=get_player_filter_params(self.mass, child_player_id) + if child_player_id + else None, + ): + try: + await resp.write(chunk) + except (BrokenPipeError, ConnectionResetError, ConnectionError): + # race condition + break + + return resp diff --git a/music_assistant/providers/slimproto/icon.svg b/music_assistant/providers/slimproto/icon.svg new file mode 100644 index 00000000..20dc9b94 --- /dev/null +++ b/music_assistant/providers/slimproto/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/music_assistant/providers/slimproto/manifest.json b/music_assistant/providers/slimproto/manifest.json new file mode 100644 index 00000000..86bda8f4 --- /dev/null +++ b/music_assistant/providers/slimproto/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "player", + "domain": "slimproto", + "name": "Slimproto (Squeezebox players)", + "description": "Support for slimproto based players (e.g. squeezebox, squeezelite).", + "codeowners": ["@music-assistant"], + "requirements": ["aioslimproto==3.1.0"], + "documentation": "https://music-assistant.io/player-support/slimproto/", + "multi_instance": false, + "builtin": false +} diff --git a/music_assistant/providers/slimproto/multi_client_stream.py b/music_assistant/providers/slimproto/multi_client_stream.py new file mode 100644 index 00000000..11acf23c --- /dev/null +++ b/music_assistant/providers/slimproto/multi_client_stream.py @@ -0,0 +1,102 @@ +"""Implementation of a simple multi-client stream task/job.""" + +import asyncio +import logging +from collections.abc import AsyncGenerator +from contextlib import suppress + +from music_assistant_models.media_items import AudioFormat + +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.util import empty_queue + +LOGGER = logging.getLogger(__name__) + + +class MultiClientStream: + """Implementation of a simple multi-client (audio) stream task/job.""" + + def __init__( + self, + audio_source: AsyncGenerator[bytes, None], + audio_format: AudioFormat, + expected_clients: int = 0, + ) -> None: + """Initialize MultiClientStream.""" + self.audio_source = audio_source + self.audio_format = audio_format + self.subscribers: list[asyncio.Queue] = [] + self.expected_clients = expected_clients + self.task = asyncio.create_task(self._runner()) + + @property + def done(self) -> bool: + """Return if this stream is already done.""" + return self.task.done() + + async def stop(self) -> None: + """Stop/cancel the stream.""" + if self.done: + return + self.task.cancel() + with suppress(asyncio.CancelledError): + await self.task + for sub_queue in list(self.subscribers): + empty_queue(sub_queue) + + async def get_stream( + self, + output_format: AudioFormat, + filter_params: list[str] | None = None, + ) -> AsyncGenerator[bytes, None]: + """Get (client specific encoded) ffmpeg stream.""" + async for chunk in get_ffmpeg_stream( + audio_input=self.subscribe_raw(), + input_format=self.audio_format, + output_format=output_format, + filter_params=filter_params, + ): + yield chunk + + async def subscribe_raw(self) -> AsyncGenerator[bytes, None]: + """Subscribe to the raw/unaltered audio stream.""" + try: + queue = asyncio.Queue(2) + self.subscribers.append(queue) + while True: + chunk = await queue.get() + if chunk == b"": + break + yield chunk + finally: + with suppress(ValueError): + self.subscribers.remove(queue) + + async def _runner(self) -> None: + """Run the stream for the given audio source.""" + expected_clients = self.expected_clients or 1 + # wait for first/all subscriber + count = 0 + while count < 50: + await asyncio.sleep(0.1) + count += 1 + if len(self.subscribers) >= expected_clients: + break + LOGGER.debug( + "Starting multi-client stream with %s/%s clients", + len(self.subscribers), + self.expected_clients, + ) + async for chunk in self.audio_source: + fail_count = 0 + while len(self.subscribers) == 0: + await asyncio.sleep(0.1) + fail_count += 1 + if fail_count > 50: + LOGGER.warning("No clients connected, stopping stream") + return + await asyncio.gather( + *[sub.put(chunk) for sub in self.subscribers], return_exceptions=True + ) + # EOF: send empty chunk + await asyncio.gather(*[sub.put(b"") for sub in self.subscribers], return_exceptions=True) diff --git a/music_assistant/providers/snapcast/__init__.py b/music_assistant/providers/snapcast/__init__.py new file mode 100644 index 00000000..f2151905 --- /dev/null +++ b/music_assistant/providers/snapcast/__init__.py @@ -0,0 +1,743 @@ +"""Snapcast Player provider for Music Assistant.""" + +from __future__ import annotations + +import asyncio +import logging +import pathlib +import random +import re +import socket +import time +from contextlib import suppress +from typing import TYPE_CHECKING, Final, cast + +from bidict import bidict +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_FLOW_MODE_ENFORCED, + ConfigEntry, + ConfigValueOption, + ConfigValueType, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ( + ConfigEntryType, + ContentType, + MediaType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderFeature, +) +from music_assistant_models.errors import SetupFailedError +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from snapcast.control import create_server +from snapcast.control.client import Snapclient +from zeroconf import NonUniqueNameException +from zeroconf.asyncio import AsyncServiceInfo + +from music_assistant.helpers.audio import FFMpeg, get_ffmpeg_stream, get_player_filter_params +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.util import get_ip_pton +from music_assistant.models.player_provider import PlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + from snapcast.control.group import Snapgroup + from snapcast.control.server import Snapserver + from snapcast.control.stream import Snapstream + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + from music_assistant.providers.player_group import PlayerGroupProvider + +CONF_SERVER_HOST = "snapcast_server_host" +CONF_SERVER_CONTROL_PORT = "snapcast_server_control_port" +CONF_USE_EXTERNAL_SERVER = "snapcast_use_external_server" +CONF_SERVER_BUFFER_SIZE = "snapcast_server_built_in_buffer_size" +CONF_SERVER_CHUNK_MS = "snapcast_server_built_in_chunk_ms" +CONF_SERVER_INITIAL_VOLUME = "snapcast_server_built_in_initial_volume" +CONF_SERVER_TRANSPORT_CODEC = "snapcast_server_built_in_codec" +CONF_SERVER_SEND_AUDIO_TO_MUTED = "snapcast_server_built_in_send_muted" +CONF_STREAM_IDLE_THRESHOLD = "snapcast_stream_idle_threshold" + + +CONF_CATEGORY_GENERIC = "generic" +CONF_CATEGORY_ADVANCED = "advanced" +CONF_CATEGORY_BUILT_IN = "Built-in Snapserver Settings" + +CONF_HELP_LINK = ( + "https://raw.githubusercontent.com/badaix/snapcast/refs/heads/master/server/etc/snapserver.conf" +) + +# airplay has fixed sample rate/bit depth so make this config entry static and hidden +CONF_ENTRY_SAMPLE_RATES_SNAPCAST = create_sample_rates_config_entry(48000, 16, 48000, 16, True) + +DEFAULT_SNAPSERVER_IP = "127.0.0.1" +DEFAULT_SNAPSERVER_PORT = 1705 +DEFAULT_SNAPSTREAM_IDLE_THRESHOLD = 60000 + +SNAPWEB_DIR: Final[pathlib.Path] = pathlib.Path(__file__).parent.resolve().joinpath("snapweb") + + +DEFAULT_SNAPCAST_FORMAT = AudioFormat( + content_type=ContentType.PCM_S16LE, + sample_rate=48000, + # TODO: can we handle 24 bits bit depth ? + bit_depth=16, + channels=2, +) + +DEFAULT_SNAPCAST_PCM_FORMAT = AudioFormat( + # the format that is used as intermediate pcm stream, + # we prefer F32 here to account for volume normalization + content_type=ContentType.PCM_F32LE, + sample_rate=48000, + bit_depth=32, + channels=2, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return SnapCastProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, # noqa: ARG001 + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + returncode, output = await check_output("snapserver", "-v") + snapserver_version = int(output.decode().split(".")[1]) if returncode == 0 else -1 + local_snapserver_present = snapserver_version >= 27 + if returncode == 0 and not local_snapserver_present: + raise SetupFailedError("Invalid snapserver version") + + return ( + ConfigEntry( + key=CONF_SERVER_BUFFER_SIZE, + type=ConfigEntryType.INTEGER, + range=(200, 6000), + default_value=1000, + label="Snapserver buffer size", + required=False, + category=CONF_CATEGORY_BUILT_IN, + hidden=not local_snapserver_present, + help_link=CONF_HELP_LINK, + ), + ConfigEntry( + key=CONF_SERVER_CHUNK_MS, + type=ConfigEntryType.INTEGER, + range=(10, 100), + default_value=26, + label="Snapserver chunk size", + required=False, + category=CONF_CATEGORY_BUILT_IN, + hidden=not local_snapserver_present, + help_link=CONF_HELP_LINK, + ), + ConfigEntry( + key=CONF_SERVER_INITIAL_VOLUME, + type=ConfigEntryType.INTEGER, + range=(0, 100), + default_value=25, + label="Snapserver initial volume", + required=False, + category=CONF_CATEGORY_BUILT_IN, + hidden=not local_snapserver_present, + help_link=CONF_HELP_LINK, + ), + ConfigEntry( + key=CONF_SERVER_SEND_AUDIO_TO_MUTED, + type=ConfigEntryType.BOOLEAN, + default_value=False, + label="Send audio to muted clients", + required=False, + category=CONF_CATEGORY_BUILT_IN, + hidden=not local_snapserver_present, + help_link=CONF_HELP_LINK, + ), + ConfigEntry( + key=CONF_SERVER_TRANSPORT_CODEC, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption( + title="FLAC", + value="flac", + ), + ConfigValueOption( + title="OGG", + value="ogg", + ), + ConfigValueOption( + title="OPUS", + value="opus", + ), + ConfigValueOption( + title="PCM", + value="pcm", + ), + ), + default_value="flac", + label="Snapserver default transport codec", + required=False, + category=CONF_CATEGORY_BUILT_IN, + hidden=not local_snapserver_present, + help_link=CONF_HELP_LINK, + ), + ConfigEntry( + key=CONF_USE_EXTERNAL_SERVER, + type=ConfigEntryType.BOOLEAN, + default_value=not local_snapserver_present, + label="Use existing Snapserver", + required=False, + category=( + CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC + ), + ), + ConfigEntry( + key=CONF_SERVER_HOST, + type=ConfigEntryType.STRING, + default_value=DEFAULT_SNAPSERVER_IP, + label="Snapcast server ip", + required=False, + depends_on=CONF_USE_EXTERNAL_SERVER, + category=( + CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC + ), + ), + ConfigEntry( + key=CONF_SERVER_CONTROL_PORT, + type=ConfigEntryType.INTEGER, + default_value=DEFAULT_SNAPSERVER_PORT, + label="Snapcast control port", + required=False, + depends_on=CONF_USE_EXTERNAL_SERVER, + category=( + CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC + ), + ), + ConfigEntry( + key=CONF_STREAM_IDLE_THRESHOLD, + type=ConfigEntryType.INTEGER, + default_value=DEFAULT_SNAPSTREAM_IDLE_THRESHOLD, + label="Snapcast idle threshold stream parameter", + required=True, + category=CONF_CATEGORY_ADVANCED, + ), + ) + + +class SnapCastProvider(PlayerProvider): + """Player provider for Snapcast based players.""" + + _snapserver: Snapserver + _snapcast_server_host: str + _snapcast_server_control_port: int + _stream_tasks: dict[str, asyncio.Task] + _use_builtin_server: bool + _snapserver_runner: asyncio.Task | None + _snapserver_started: asyncio.Event | None + _ids_map: bidict # ma_id / snapclient_id + _stop_called: bool + + def _get_snapclient_id(self, player_id: str) -> str: + search_dict = self._ids_map + return search_dict.get(player_id) + + def _get_ma_id(self, snap_client_id: str) -> str: + search_dict = self._ids_map.inverse + return search_dict.get(snap_client_id) + + def _generate_and_register_id(self, snap_client_id) -> str: + search_dict = self._ids_map.inverse + if snap_client_id not in search_dict: + new_id = "ma_" + str(re.sub(r"\W+", "", snap_client_id)) + self._ids_map[new_id] = snap_client_id + return new_id + else: + return self._get_ma_id(snap_client_id) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + # set snapcast logging + logging.getLogger("snapcast").setLevel(self.logger.level) + self._use_builtin_server = not self.config.get_value(CONF_USE_EXTERNAL_SERVER) + self._stop_called = False + if self._use_builtin_server: + self._snapcast_server_host = "127.0.0.1" + self._snapcast_server_control_port = DEFAULT_SNAPSERVER_PORT + self._snapcast_server_buffer_size = self.config.get_value(CONF_SERVER_BUFFER_SIZE) + self._snapcast_server_chunk_ms = self.config.get_value(CONF_SERVER_CHUNK_MS) + self._snapcast_server_initial_volume = self.config.get_value(CONF_SERVER_INITIAL_VOLUME) + self._snapcast_server_send_to_muted = self.config.get_value( + CONF_SERVER_SEND_AUDIO_TO_MUTED + ) + self._snapcast_server_transport_codec = self.config.get_value( + CONF_SERVER_TRANSPORT_CODEC + ) + + else: + self._snapcast_server_host = self.config.get_value(CONF_SERVER_HOST) + self._snapcast_server_control_port = self.config.get_value(CONF_SERVER_CONTROL_PORT) + self._snapcast_stream_idle_threshold = self.config.get_value(CONF_STREAM_IDLE_THRESHOLD) + self._stream_tasks = {} + self._ids_map = bidict({}) + + if self._use_builtin_server: + await self._start_builtin_server() + else: + self._snapserver_runner = None + self._snapserver_started = None + try: + self._snapserver = await create_server( + self.mass.loop, + self._snapcast_server_host, + port=self._snapcast_server_control_port, + reconnect=True, + ) + self._snapserver.set_on_update_callback(self._handle_update) + self.logger.info( + "Started connection to Snapserver %s", + f"{self._snapcast_server_host}:{self._snapcast_server_control_port}", + ) + # register callback for when the connection gets lost to the snapserver + self._snapserver.set_on_disconnect_callback(self._handle_disconnect) + await self._create_default_stream() + except OSError as err: + msg = "Unable to start the Snapserver connection ?" + raise SetupFailedError(msg) from err + + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + await super().loaded_in_mass() + # initial load of players + self._handle_update() + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + self._stop_called = True + for snap_client_id in self._snapserver.clients: + player_id = self._get_ma_id(snap_client_id) + await self.cmd_stop(player_id) + self._snapserver.stop() + await self._stop_builtin_server() + + def _handle_update(self) -> None: + """Process Snapcast init Player/Group and set callback .""" + for snap_client in self._snapserver.clients: + self._handle_player_init(snap_client) + snap_client.set_callback(self._handle_player_update) + for snap_client in self._snapserver.clients: + self._handle_player_update(snap_client) + for snap_group in self._snapserver.groups: + snap_group.set_callback(self._handle_group_update) + + def _handle_group_update(self, snap_group: Snapgroup) -> None: + """Process Snapcast group callback.""" + for snap_client in self._snapserver.clients: + self._handle_player_update(snap_client) + + def _handle_player_init(self, snap_client: Snapclient) -> None: + """Process Snapcast add to Player controller.""" + player_id = self._generate_and_register_id(snap_client.identifier) + player = self.mass.players.get(player_id, raise_unavailable=False) + if not player: + snap_client = cast( + Snapclient, self._snapserver.client(self._get_snapclient_id(player_id)) + ) + player = Player( + player_id=player_id, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=snap_client.friendly_name, + available=True, + powered=snap_client.connected, + device_info=DeviceInfo( + model=snap_client._client.get("host").get("os"), + address=snap_client._client.get("host").get("ip"), + manufacturer=snap_client._client.get("host").get("arch"), + ), + supported_features=( + PlayerFeature.SYNC, + PlayerFeature.VOLUME_SET, + PlayerFeature.VOLUME_MUTE, + ), + group_childs=set(), + synced_to=self._synced_to(player_id), + ) + asyncio.run_coroutine_threadsafe( + self.mass.players.register_or_update(player), loop=self.mass.loop + ) + + def _handle_player_update(self, snap_client: Snapclient) -> None: + """Process Snapcast update to Player controller.""" + player_id = self._get_ma_id(snap_client.identifier) + player = self.mass.players.get(player_id) + if not player: + return + player.name = snap_client.friendly_name + player.volume_level = snap_client.volume + player.volume_muted = snap_client.muted + player.available = snap_client.connected + player.synced_to = self._synced_to(player_id) + if player.active_group is None: + if stream := self._get_snapstream(player_id): + if stream.name.startswith(("MusicAssistant", "default")): + player.active_source = player_id + else: + player.active_source = stream.name + else: + player.active_source = player_id + self._group_childs(player_id) + self.mass.players.update(player_id) + + async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: + """Return all (provider/player specific) Config Entries for the given player (if any).""" + base_entries = await super().get_player_config_entries(player_id) + return ( + *base_entries, + CONF_ENTRY_FLOW_MODE_ENFORCED, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_SAMPLE_RATES_SNAPCAST, + ) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + snap_client_id = self._get_snapclient_id(player_id) + await self._snapserver.client(snap_client_id).set_volume(volume_level) + self.mass.players.update(snap_client_id) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + player = self.mass.players.get(player_id, raise_unavailable=False) + if stream_task := self._stream_tasks.pop(player_id, None): + if not stream_task.done(): + stream_task.cancel() + player.state = PlayerState.IDLE + self._set_childs_state(player_id) + self.mass.players.update(player_id) + # assign default/empty stream to the player + await self._get_snapgroup(player_id).set_stream("default") + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send MUTE command to given player.""" + ma_player = self.mass.players.get(player_id, raise_unavailable=False) + snap_client_id = self._get_snapclient_id(player_id) + snapclient = self._snapserver.client(snap_client_id) + # Using optimistic value because the library does not return the response from the api + await snapclient.set_muted(muted) + ma_player.volume_muted = snapclient.muted + self.mass.players.update(player_id) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Sync Snapcast player.""" + group = self._get_snapgroup(target_player) + mass_target_player = self.mass.players.get(target_player) + if self._get_snapclient_id(player_id) not in group.clients: + await group.add_client(self._get_snapclient_id(player_id)) + mass_player = self.mass.players.get(player_id) + mass_player.synced_to = target_player + mass_target_player.group_childs.add(player_id) + self.mass.players.update(player_id) + self.mass.players.update(target_player) + + async def cmd_unsync(self, player_id: str) -> None: + """Unsync Snapcast player.""" + mass_player = self.mass.players.get(player_id) + if mass_player.synced_to is None: + for mass_child_id in list(mass_player.group_childs): + if mass_child_id != player_id: + await self.cmd_unsync(mass_child_id) + return + mass_sync_master_player = self.mass.players.get(mass_player.synced_to) + mass_sync_master_player.group_childs.remove(player_id) + mass_player.synced_to = None + snap_client_id = self._get_snapclient_id(player_id) + group = self._get_snapgroup(player_id) + await group.remove_client(snap_client_id) + # assign default/empty stream to the player + await self._get_snapgroup(player_id).set_stream("default") + await self.cmd_stop(player_id=player_id) + # make sure that the player manager gets an update + self.mass.players.update(player_id, skip_forward=True) + self.mass.players.update(mass_player.synced_to, skip_forward=True) + + async def play_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle PLAY MEDIA on given player.""" + player = self.mass.players.get(player_id) + if player.synced_to: + msg = "A synced player cannot receive play commands directly" + raise RuntimeError(msg) + # stop any existing streams first + if stream_task := self._stream_tasks.pop(player_id, None): + if not stream_task.done(): + stream_task.cancel() + # initialize a new stream and attach it to the group + stream, port = await self._create_stream() + snap_group = self._get_snapgroup(player_id) + await snap_group.set_stream(stream.identifier) + + # select audio source + if media.media_type == MediaType.ANNOUNCEMENT: + # special case: stream announcement + input_format = DEFAULT_SNAPCAST_FORMAT + audio_source = self.mass.streams.get_announcement_stream( + media.custom_data["url"], + output_format=DEFAULT_SNAPCAST_FORMAT, + use_pre_announce=media.custom_data["use_pre_announce"], + ) + elif media.queue_id.startswith("ugp_"): + # special case: UGP stream + ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") + ugp_stream = ugp_provider.ugp_streams[media.queue_id] + input_format = ugp_stream.output_format + audio_source = ugp_stream.subscribe() + elif media.queue_id and media.queue_item_id: + # regular queue (flow) stream request + input_format = DEFAULT_SNAPCAST_PCM_FORMAT + audio_source = self.mass.streams.get_flow_stream( + queue=self.mass.player_queues.get(media.queue_id), + start_queue_item=self.mass.player_queues.get_item( + media.queue_id, media.queue_item_id + ), + pcm_format=input_format, + ) + else: + # assume url or some other direct path + # NOTE: this will fail if its an uri not playable by ffmpeg + input_format = DEFAULT_SNAPCAST_FORMAT + audio_source = get_ffmpeg_stream( + audio_input=media.uri, + input_format=AudioFormat(ContentType.try_parse(media.uri)), + output_format=DEFAULT_SNAPCAST_FORMAT, + ) + + async def _streamer() -> None: + host = self._snapcast_server_host + stream_path = f"tcp://{host}:{port}" + self.logger.debug("Start streaming to %s", stream_path) + try: + async with FFMpeg( + audio_input=audio_source, + input_format=input_format, + output_format=DEFAULT_SNAPCAST_FORMAT, + filter_params=get_player_filter_params(self.mass, player_id), + audio_output=stream_path, + ) as ffmpeg_proc: + player.state = PlayerState.PLAYING + player.current_media = media + player.elapsed_time = 0 + player.elapsed_time_last_updated = time.time() + self.mass.players.update(player_id) + self._set_childs_state(player_id) + await ffmpeg_proc.wait() + self.logger.debug("Finished streaming to %s", stream_path) + # we need to wait a bit for the stream status to become idle + # to ensure that all snapclients have consumed the audio + while stream.status != "idle": + await asyncio.sleep(0.25) + player.state = PlayerState.IDLE + self.mass.players.update(player_id) + self._set_childs_state(player_id) + finally: + await self._delete_current_snapstream(stream, media) + + # start streaming the queue (pcm) audio in a background task + self._stream_tasks[player_id] = asyncio.create_task(_streamer()) + + async def _delete_current_snapstream(self, stream: Snapstream, media: PlayerMedia) -> None: + with suppress(TypeError, KeyError, AttributeError): + if media.duration < 5: + await asyncio.sleep(5) + await self._snapserver.stream_remove_stream(stream.identifier) + + def _get_snapgroup(self, player_id: str) -> Snapgroup: + """Get snapcast group for given player_id.""" + snap_client_id = self._get_snapclient_id(player_id) + client: Snapclient = self._snapserver.client(snap_client_id) + return client.group + + def _get_snapstream(self, player_id: str) -> Snapstream | None: + """Get snapcast stream for given player_id.""" + if group := self._get_snapgroup(player_id): + with suppress(KeyError): + return self._snapserver.stream(group.stream) + return None + + def _synced_to(self, player_id: str) -> str | None: + """Return player_id of the player this player is synced to.""" + snap_group: Snapgroup = self._get_snapgroup(player_id) + master_id: str = self._get_ma_id(snap_group.clients[0]) + + if len(snap_group.clients) < 2 or player_id == master_id: + return None + return master_id + + def _group_childs(self, player_id: str) -> set[str]: + """Return player_ids of the players synced to this player.""" + mass_player = self.mass.players.get(player_id, raise_unavailable=False) + snap_group = self._get_snapgroup(player_id) + mass_player.group_childs.clear() + if mass_player.synced_to is not None: + return + mass_player.group_childs.add(player_id) + { + mass_player.group_childs.add(self._get_ma_id(snap_client_id)) + for snap_client_id in snap_group.clients + if self._get_ma_id(snap_client_id) != player_id + and self._snapserver.client(snap_client_id).connected + } + + async def _create_stream(self) -> tuple[Snapstream, int]: + """Create new stream on snapcast server.""" + attempts = 50 + while attempts: + attempts -= 1 + # pick a random port + port = random.randint(4953, 4953 + 200) + name = f"MusicAssistant--{port}" + result = await self._snapserver.stream_add_stream( + # NOTE: setting the sampleformat to something else + # (like 24 bits bit depth) does not seem to work at all! + f"tcp://0.0.0.0:{port}?name={name}&sampleformat=48000:16:2&idle_threshold={self._snapcast_stream_idle_threshold}", + ) + if "id" not in result: + # if the port is already taken, the result will be an error + self.logger.warning(result) + continue + stream = self._snapserver.stream(result["id"]) + return (stream, port) + msg = "Unable to create stream - No free port found?" + raise RuntimeError(msg) + + async def _create_default_stream(self) -> None: + """Create new stream on snapcast server named default case not exist.""" + all_streams = {stream.name for stream in self._snapserver.streams} + if "default" not in all_streams: + await self._snapserver.stream_add_stream( + "pipe:///tmp/snapfifo?name=default&sampleformat=48000:16:2" + ) + + def _set_childs_state(self, player_id: str) -> None: + """Set the state of the child`s of the player.""" + mass_player = self.mass.players.get(player_id) + for child_player_id in mass_player.group_childs: + if child_player_id == player_id: + continue + mass_child_player = self.mass.players.get(child_player_id) + mass_child_player.state = mass_player.state + self.mass.players.update(child_player_id) + + async def _builtin_server_runner(self) -> None: + """Start running the builtin snapserver.""" + if self._snapserver_started.is_set(): + raise RuntimeError("Snapserver is already started!") + logger = self.logger.getChild("snapserver") + logger.info("Starting builtin Snapserver...") + # register the snapcast mdns services + for name, port in ( + ("-http", 1780), + ("-jsonrpc", 1705), + ("-stream", 1704), + ("-tcp", 1705), + ("", 1704), + ): + zeroconf_type = f"_snapcast{name}._tcp.local." + try: + info = AsyncServiceInfo( + zeroconf_type, + name=f"Snapcast.{zeroconf_type}", + properties={"is_mass": "true"}, + addresses=[await get_ip_pton(self.mass.streams.publish_ip)], + port=port, + server=f"{socket.gethostname()}.local", + ) + attr_name = f"zc_service_set{name}" + if getattr(self, attr_name, None): + await self.mass.aiozc.async_update_service(info) + else: + await self.mass.aiozc.async_register_service(info, strict=False) + setattr(self, attr_name, True) + except NonUniqueNameException: + self.logger.debug( + "Could not register mdns record for %s as its already in use", + zeroconf_type, + ) + except Exception as err: + self.logger.exception( + "Could not register mdns record for %s: %s", zeroconf_type, str(err) + ) + args = [ + "snapserver", + # config settings taken from + # https://raw.githubusercontent.com/badaix/snapcast/86cd4b2b63e750a72e0dfe6a46d47caf01426c8d/server/etc/snapserver.conf + f"--server.datadir={self.mass.storage_path}", + "--http.enabled=true", + "--http.port=1780", + f"--http.doc_root={SNAPWEB_DIR}", + "--tcp.enabled=true", + f"--tcp.port={self._snapcast_server_control_port}", + f"--stream.buffer={self._snapcast_server_buffer_size}", + f"--stream.chunk_ms={self._snapcast_server_chunk_ms}", + f"--stream.codec={self._snapcast_server_transport_codec}", + f"--stream.send_to_muted={str(self._snapcast_server_send_to_muted).lower()}", + f"--streaming_client.initial_volume={self._snapcast_server_initial_volume}", + ] + async with AsyncProcess(args, stdout=True, name="snapserver") as snapserver_proc: + # keep reading from stdout until exit + async for data in snapserver_proc.iter_any(): + data = data.decode().strip() # noqa: PLW2901 + for line in data.split("\n"): + logger.debug(line) + if "(Snapserver) Version 0." in line: + # delay init a small bit to prevent race conditions + # where we try to connect too soon + self.mass.loop.call_later(2, self._snapserver_started.set) + + async def _stop_builtin_server(self) -> None: + """Stop the built-in Snapserver.""" + self.logger.info("Stopping, built-in Snapserver") + if self._snapserver_runner and not self._snapserver_runner.done(): + self._snapserver_runner.cancel() + self._snapserver_started.clear() + + async def _start_builtin_server(self) -> None: + """Start the built-in Snapserver.""" + if self._use_builtin_server: + self._snapserver_started = asyncio.Event() + self._snapserver_runner = asyncio.create_task(self._builtin_server_runner()) + await asyncio.wait_for(self._snapserver_started.wait(), 10) + + def _handle_disconnect(self, exc: Exception) -> None: + """Handle disconnect callback from snapserver.""" + if self._stop_called: + # we're instructed to stop/exit, so no need to restart the connection + return + self.logger.info( + "Connection to SnapServer lost, reason: %s. Reloading provider in 5 seconds.", + str(exc), + ) + # schedule a reload of the provider + self.mass.call_later(5, self.mass.load_provider, self.instance_id, allow_retry=True) diff --git a/music_assistant/providers/snapcast/icon.svg b/music_assistant/providers/snapcast/icon.svg new file mode 100644 index 00000000..853f3659 --- /dev/null +++ b/music_assistant/providers/snapcast/icon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/music_assistant/providers/snapcast/manifest.json b/music_assistant/providers/snapcast/manifest.json new file mode 100644 index 00000000..0f75f6f2 --- /dev/null +++ b/music_assistant/providers/snapcast/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "player", + "domain": "snapcast", + "name": "Snapcast", + "description": "Support for snapcast server and clients.", + "codeowners": ["@SantigoSotoC"], + "requirements": ["snapcast==2.3.6", "bidict==0.23.1"], + "documentation": "https://music-assistant.io/player-support/snapcast/", + "multi_instance": false, + "builtin": false +} diff --git a/music_assistant/providers/snapcast/snapweb/10-seconds-of-silence.mp3 b/music_assistant/providers/snapcast/snapweb/10-seconds-of-silence.mp3 new file mode 100644 index 00000000..40361eca Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/10-seconds-of-silence.mp3 differ diff --git a/music_assistant/providers/snapcast/snapweb/3rd-party/libflac.js b/music_assistant/providers/snapcast/snapweb/3rd-party/libflac.js new file mode 100644 index 00000000..c320c8c0 --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/3rd-party/libflac.js @@ -0,0 +1,34568 @@ + + +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(Module) { ..generated code.. } +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = typeof Module !== 'undefined' ? Module : {}; + + + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +// libflac.js - port of libflac to JavaScript using emscripten + + +(function (root, factory) { + + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['module', 'require'], factory.bind(null, root)); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + + // use process.env (if available) for reading Flac environment settings: + var env = typeof process !== 'undefined' && process && process.env? process.env : root; + factory(env, module, module.require); + } else { + // Browser globals + root.Flac = factory(root); + } + +}(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this, function (global, expLib, require) { +'use strict'; + +var Module = Module || {}; +var _flac_ready = false; +//in case resources are loaded asynchronously (e.g. *.mem file for minified version): setup "ready" handling +Module["onRuntimeInitialized"] = function(){ + _flac_ready = true; + if(!_exported){ + //if _exported is not yet set (may happen, in case initialization was strictly synchronously), + // do "pause" until sync initialization has run through + setTimeout(function(){do_fire_event('ready', [{type: 'ready', target: _exported}], true);}, 0); + } else { + do_fire_event('ready', [{type: 'ready', target: _exported}], true); + } +}; + +if(global && global.FLAC_SCRIPT_LOCATION){ + + Module["locateFile"] = function(fileName){ + var path = global.FLAC_SCRIPT_LOCATION || ''; + if(path[fileName]){ + return path[fileName]; + } + path += path && !/\/$/.test(path)? '/' : ''; + return path + fileName; + }; + + //NOTE will be overwritten if emscripten has env specific implementation for this + var readBinary = function(filePath){ + + //for Node: use default implementation (copied from generated code): + if(ENVIRONMENT_IS_NODE){ + var ret = read_(filePath, true); + if (!ret.buffer) { + ret = new Uint8Array(ret); + } + assert(ret.buffer); + return ret; + } + + //otherwise: try "fallback" to AJAX + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + xhr.responseType = "arraybuffer"; + xhr.addEventListener("load", function(evt){ + resolve(xhr.response); + }); + xhr.addEventListener("error", function(err){ + reject(err); + }); + xhr.open("GET", filePath); + xhr.send(); + }); + }; +} + +//fallback for fetch && support file://-protocol: try read as binary if fetch fails +if(global && typeof global.fetch === 'function'){ + var _fetch = global.fetch; + global.fetch = function(url){ + return _fetch.apply(null, arguments).catch(function(err){ + try{ + var result = readBinary(url); + if(result && result.catch){ + result.catch(function(_err){throw err}); + } + return result; + } catch(_err){ + throw err; + } + }); + }; +} + + + +// Sometimes an existing Module object exists with properties +// meant to overwrite the default module functionality. Here +// we collect those properties and reapply _after_ we configure +// the current environment's defaults to avoid having to be so +// defensive during initialization. +var moduleOverrides = {}; +var key; +for (key in Module) { + if (Module.hasOwnProperty(key)) { + moduleOverrides[key] = Module[key]; + } +} + +var arguments_ = []; +var thisProgram = './this.program'; +var quit_ = function(status, toThrow) { + throw toThrow; +}; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +var ENVIRONMENT_IS_WEB = false; +var ENVIRONMENT_IS_WORKER = false; +var ENVIRONMENT_IS_NODE = false; +var ENVIRONMENT_IS_SHELL = false; +ENVIRONMENT_IS_WEB = typeof window === 'object'; +ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'; +ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + + + + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ''; +function locateFile(path) { + if (Module['locateFile']) { + return Module['locateFile'](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var read_, + readAsync, + readBinary, + setWindowTitle; + +var nodeFS; +var nodePath; + +if (ENVIRONMENT_IS_NODE) { + if (ENVIRONMENT_IS_WORKER) { + scriptDirectory = require('path').dirname(scriptDirectory) + '/'; + } else { + scriptDirectory = __dirname + '/'; + } + + + + + read_ = function shell_read(filename, binary) { + var ret = tryParseAsDataURI(filename); + if (ret) { + return binary ? ret : ret.toString(); + } + if (!nodeFS) nodeFS = require('fs'); + if (!nodePath) nodePath = require('path'); + filename = nodePath['normalize'](filename); + return nodeFS['readFileSync'](filename, binary ? null : 'utf8'); + }; + + readBinary = function readBinary(filename) { + var ret = read_(filename, true); + if (!ret.buffer) { + ret = new Uint8Array(ret); + } + assert(ret.buffer); + return ret; + }; + + + + + if (process['argv'].length > 1) { + thisProgram = process['argv'][1].replace(/\\/g, '/'); + } + + arguments_ = process['argv'].slice(2); + + if (typeof module !== 'undefined') { + module['exports'] = Module; + } + + + + quit_ = function(status) { + process['exit'](status); + }; + + Module['inspect'] = function () { return '[Emscripten Module object]'; }; + + + +} else +if (ENVIRONMENT_IS_SHELL) { + + + if (typeof read != 'undefined') { + read_ = function shell_read(f) { + var data = tryParseAsDataURI(f); + if (data) { + return intArrayToString(data); + } + return read(f); + }; + } + + readBinary = function readBinary(f) { + var data; + data = tryParseAsDataURI(f); + if (data) { + return data; + } + if (typeof readbuffer === 'function') { + return new Uint8Array(readbuffer(f)); + } + data = read(f, 'binary'); + assert(typeof data === 'object'); + return data; + }; + + if (typeof scriptArgs != 'undefined') { + arguments_ = scriptArgs; + } else if (typeof arguments != 'undefined') { + arguments_ = arguments; + } + + if (typeof quit === 'function') { + quit_ = function(status) { + quit(status); + }; + } + + if (typeof print !== 'undefined') { + // Prefer to use print/printErr where they exist, as they usually work better. + if (typeof console === 'undefined') console = /** @type{!Console} */({}); + console.log = /** @type{!function(this:Console, ...*): undefined} */ (print); + console.warn = console.error = /** @type{!function(this:Console, ...*): undefined} */ (typeof printErr !== 'undefined' ? printErr : print); + } + + +} else + +// Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled + scriptDirectory = self.location.href; + } else if (document.currentScript) { // web + scriptDirectory = document.currentScript.src; + } + // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. + // otherwise, slice off the final part of the url to find the script directory. + // if scriptDirectory does not contain a slash, lastIndexOf will return -1, + // and scriptDirectory will correctly be replaced with an empty string. + if (scriptDirectory.indexOf('blob:') !== 0) { + scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf('/')+1); + } else { + scriptDirectory = ''; + } + + + // Differentiate the Web Worker from the Node Worker case, as reading must + // be done differently. + { + + + + + read_ = function shell_read(url) { + try { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.send(null); + return xhr.responseText; + } catch (err) { + var data = tryParseAsDataURI(url); + if (data) { + return intArrayToString(data); + } + throw err; + } + }; + + if (ENVIRONMENT_IS_WORKER) { + readBinary = function readBinary(url) { + try { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.responseType = 'arraybuffer'; + xhr.send(null); + return new Uint8Array(/** @type{!ArrayBuffer} */(xhr.response)); + } catch (err) { + var data = tryParseAsDataURI(url); + if (data) { + return data; + } + throw err; + } + }; + } + + readAsync = function readAsync(url, onload, onerror) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function xhr_onload() { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 + onload(xhr.response); + return; + } + var data = tryParseAsDataURI(url); + if (data) { + onload(data.buffer); + return; + } + onerror(); + }; + xhr.onerror = onerror; + xhr.send(null); + }; + + + + + } + + setWindowTitle = function(title) { document.title = title }; +} else +{ +} + + +// Set up the out() and err() hooks, which are how we can print to stdout or +// stderr, respectively. +var out = Module['print'] || console.log.bind(console); +var err = Module['printErr'] || console.warn.bind(console); + +// Merge back in the overrides +for (key in moduleOverrides) { + if (moduleOverrides.hasOwnProperty(key)) { + Module[key] = moduleOverrides[key]; + } +} +// Free the object hierarchy contained in the overrides, this lets the GC +// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. +moduleOverrides = null; + +// Emit code to handle expected values on the Module object. This applies Module.x +// to the proper local x. This has two benefits: first, we only emit it if it is +// expected to arrive, and second, by using a local everywhere else that can be +// minified. +if (Module['arguments']) arguments_ = Module['arguments']; +if (Module['thisProgram']) thisProgram = Module['thisProgram']; +if (Module['quit']) quit_ = Module['quit']; + +// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message + + + + + +// {{PREAMBLE_ADDITIONS}} + +var STACK_ALIGN = 16; + +function dynamicAlloc(size) { + var ret = HEAP32[DYNAMICTOP_PTR>>2]; + var end = (ret + size + 15) & -16; + HEAP32[DYNAMICTOP_PTR>>2] = end; + return ret; +} + +function alignMemory(size, factor) { + if (!factor) factor = STACK_ALIGN; // stack alignment (16-byte) by default + return Math.ceil(size / factor) * factor; +} + +function getNativeTypeSize(type) { + switch (type) { + case 'i1': case 'i8': return 1; + case 'i16': return 2; + case 'i32': return 4; + case 'i64': return 8; + case 'float': return 4; + case 'double': return 8; + default: { + if (type[type.length-1] === '*') { + return 4; // A pointer + } else if (type[0] === 'i') { + var bits = Number(type.substr(1)); + assert(bits % 8 === 0, 'getNativeTypeSize invalid bits ' + bits + ', type ' + type); + return bits / 8; + } else { + return 0; + } + } + } +} + +function warnOnce(text) { + if (!warnOnce.shown) warnOnce.shown = {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + err(text); + } +} + + + + + + + + +// Wraps a JS function as a wasm function with a given signature. +function convertJsFunctionToWasm(func, sig) { + return func; +} + +var freeTableIndexes = []; + +// Weak map of functions in the table to their indexes, created on first use. +var functionsInTableMap; + +// Add a wasm function to the table. +function addFunctionWasm(func, sig) { + var table = wasmTable; + + // Check if the function is already in the table, to ensure each function + // gets a unique index. First, create the map if this is the first use. + if (!functionsInTableMap) { + functionsInTableMap = new WeakMap(); + for (var i = 0; i < table.length; i++) { + var item = table.get(i); + // Ignore null values. + if (item) { + functionsInTableMap.set(item, i); + } + } + } + if (functionsInTableMap.has(func)) { + return functionsInTableMap.get(func); + } + + // It's not in the table, add it now. + + + var ret; + // Reuse a free index if there is one, otherwise grow. + if (freeTableIndexes.length) { + ret = freeTableIndexes.pop(); + } else { + ret = table.length; + // Grow the table + try { + table.grow(1); + } catch (err) { + if (!(err instanceof RangeError)) { + throw err; + } + throw 'Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.'; + } + } + + // Set the new value. + try { + // Attempting to call this with JS function will cause of table.set() to fail + table.set(ret, func); + } catch (err) { + if (!(err instanceof TypeError)) { + throw err; + } + var wrapped = convertJsFunctionToWasm(func, sig); + table.set(ret, wrapped); + } + + functionsInTableMap.set(func, ret); + + return ret; +} + +function removeFunctionWasm(index) { + functionsInTableMap.delete(wasmTable.get(index)); + freeTableIndexes.push(index); +} + +// 'sig' parameter is required for the llvm backend but only when func is not +// already a WebAssembly function. +function addFunction(func, sig) { + + return addFunctionWasm(func, sig); +} + +function removeFunction(index) { + removeFunctionWasm(index); +} + + + +var funcWrappers = {}; + +function getFuncWrapper(func, sig) { + if (!func) return; // on null pointer, return undefined + assert(sig); + if (!funcWrappers[sig]) { + funcWrappers[sig] = {}; + } + var sigCache = funcWrappers[sig]; + if (!sigCache[func]) { + // optimize away arguments usage in common cases + if (sig.length === 1) { + sigCache[func] = function dynCall_wrapper() { + return dynCall(sig, func); + }; + } else if (sig.length === 2) { + sigCache[func] = function dynCall_wrapper(arg) { + return dynCall(sig, func, [arg]); + }; + } else { + // general case + sigCache[func] = function dynCall_wrapper() { + return dynCall(sig, func, Array.prototype.slice.call(arguments)); + }; + } + } + return sigCache[func]; +} + + + + + + + +function makeBigInt(low, high, unsigned) { + return unsigned ? ((+((low>>>0)))+((+((high>>>0)))*4294967296.0)) : ((+((low>>>0)))+((+((high|0)))*4294967296.0)); +} + +/** @param {Array=} args */ +function dynCall(sig, ptr, args) { + if (args && args.length) { + return Module['dynCall_' + sig].apply(null, [ptr].concat(args)); + } else { + return Module['dynCall_' + sig].call(null, ptr); + } +} + +var tempRet0 = 0; + +var setTempRet0 = function(value) { + tempRet0 = value; +}; + +var getTempRet0 = function() { + return tempRet0; +}; + + +// The address globals begin at. Very low in memory, for code size and optimization opportunities. +// Above 0 is static memory, starting with globals. +// Then the stack. +// Then 'dynamic' memory for sbrk. +var GLOBAL_BASE = 1024; + + + + + +// === Preamble library stuff === + +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html + + +var wasmBinary;if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; +var noExitRuntime;if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; + + + + +// wasm2js.js - enough of a polyfill for the WebAssembly object so that we can load +// wasm2js code that way. + +// Emit "var WebAssembly" if definitely using wasm2js. Otherwise, in MAYBE_WASM2JS +// mode, we can't use a "var" since it would prevent normal wasm from working. +/** @suppress{const} */ +var +WebAssembly = { + // Note that we do not use closure quoting (this['buffer'], etc.) on these + // functions, as they are just meant for internal use. In other words, this is + // not a fully general polyfill. + Memory: function(opts) { + this.buffer = new ArrayBuffer(opts['initial'] * 65536); + this.grow = function(amount) { + var ret = __growWasmMemory(amount); + return ret; + }; + }, + + // Table is not a normal constructor and instead returns the array object. + // That lets us use the length property automatically, which is simpler and + // smaller (but instanceof will not report that an instance of Table is an + // instance of this function). + Table: /** @constructor */ function(opts) { + var ret = new Array(opts['initial']); + ret.grow = function(by) { + if (ret.length >= 22 + 5) { + abort('Unable to grow wasm table. Use a higher value for RESERVED_FUNCTION_POINTERS or set ALLOW_TABLE_GROWTH.') + } + ret.push(null); + }; + ret.set = function(i, func) { + ret[i] = func; + }; + ret.get = function(i) { + return ret[i]; + }; + return ret; + }, + + Module: function(binary) { + // TODO: use the binary and info somehow - right now the wasm2js output is embedded in + // the main JS + }, + + Instance: function(module, info) { + // TODO: use the module and info somehow - right now the wasm2js output is embedded in + // the main JS + // This will be replaced by the actual wasm2js code. + this.exports = ( +function instantiate(asmLibraryArg, wasmMemory, wasmTable) { + + + var scratchBuffer = new ArrayBuffer(8); + var i32ScratchView = new Int32Array(scratchBuffer); + var f32ScratchView = new Float32Array(scratchBuffer); + var f64ScratchView = new Float64Array(scratchBuffer); + + function wasm2js_scratch_load_i32(index) { + return i32ScratchView[index]; + } + + function wasm2js_scratch_store_i32(index, value) { + i32ScratchView[index] = value; + } + + function wasm2js_scratch_load_f64() { + return f64ScratchView[0]; + } + + function wasm2js_scratch_store_f64(value) { + f64ScratchView[0] = value; + } + + function wasm2js_scratch_store_f32(value) { + f32ScratchView[0] = value; + } + +function asmFunc(global, env, buffer) { + var memory = env.memory; + var FUNCTION_TABLE = wasmTable; + var HEAP8 = new global.Int8Array(buffer); + var HEAP16 = new global.Int16Array(buffer); + var HEAP32 = new global.Int32Array(buffer); + var HEAPU8 = new global.Uint8Array(buffer); + var HEAPU16 = new global.Uint16Array(buffer); + var HEAPU32 = new global.Uint32Array(buffer); + var HEAPF32 = new global.Float32Array(buffer); + var HEAPF64 = new global.Float64Array(buffer); + var Math_imul = global.Math.imul; + var Math_fround = global.Math.fround; + var Math_abs = global.Math.abs; + var Math_clz32 = global.Math.clz32; + var Math_min = global.Math.min; + var Math_max = global.Math.max; + var Math_floor = global.Math.floor; + var Math_ceil = global.Math.ceil; + var Math_sqrt = global.Math.sqrt; + var abort = env.abort; + var nan = global.NaN; + var infinity = global.Infinity; + var emscripten_resize_heap = env.emscripten_resize_heap; + var emscripten_memcpy_big = env.emscripten_memcpy_big; + var __wasi_fd_close = env.fd_close; + var __wasi_fd_read = env.fd_read; + var round = env.round; + var __wasi_fd_write = env.fd_write; + var setTempRet0 = env.setTempRet0; + var legalimport$__wasi_fd_seek = env.fd_seek; + var global$0 = 5257216; + var global$1 = 14168; + var __wasm_intrinsics_temp_i64 = 0; + var __wasm_intrinsics_temp_i64$hi = 0; + var i64toi32_i32$HIGH_BITS = 0; + // EMSCRIPTEN_START_FUNCS +; + function __wasm_call_ctors() { + + } + + function __errno_location() { + return 11584; + } + + function sbrk($0) { + var $1 = 0, $2 = 0; + $1 = HEAP32[3544]; + $2 = $0 + 3 & -4; + $0 = $1 + $2 | 0; + label$1 : { + if ($0 >>> 0 <= $1 >>> 0 ? ($2 | 0) >= 1 : 0) { + break label$1 + } + if ($0 >>> 0 > __wasm_memory_size() << 16 >>> 0) { + if (!emscripten_resize_heap($0 | 0)) { + break label$1 + } + } + HEAP32[3544] = $0; + return $1; + } + HEAP32[2896] = 48; + return -1; + } + + function memset($0, $1) { + var $2 = 0, $3 = 0; + label$1 : { + if (!$1) { + break label$1 + } + $2 = $0 + $1 | 0; + HEAP8[$2 + -1 | 0] = 0; + HEAP8[$0 | 0] = 0; + if ($1 >>> 0 < 3) { + break label$1 + } + HEAP8[$2 + -2 | 0] = 0; + HEAP8[$0 + 1 | 0] = 0; + HEAP8[$2 + -3 | 0] = 0; + HEAP8[$0 + 2 | 0] = 0; + if ($1 >>> 0 < 7) { + break label$1 + } + HEAP8[$2 + -4 | 0] = 0; + HEAP8[$0 + 3 | 0] = 0; + if ($1 >>> 0 < 9) { + break label$1 + } + $3 = 0 - $0 & 3; + $2 = $3 + $0 | 0; + HEAP32[$2 >> 2] = 0; + $3 = $1 - $3 & -4; + $1 = $3 + $2 | 0; + HEAP32[$1 + -4 >> 2] = 0; + if ($3 >>> 0 < 9) { + break label$1 + } + HEAP32[$2 + 8 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + HEAP32[$1 + -8 >> 2] = 0; + HEAP32[$1 + -12 >> 2] = 0; + if ($3 >>> 0 < 25) { + break label$1 + } + HEAP32[$2 + 24 >> 2] = 0; + HEAP32[$2 + 20 >> 2] = 0; + HEAP32[$2 + 16 >> 2] = 0; + HEAP32[$2 + 12 >> 2] = 0; + HEAP32[$1 + -16 >> 2] = 0; + HEAP32[$1 + -20 >> 2] = 0; + HEAP32[$1 + -24 >> 2] = 0; + HEAP32[$1 + -28 >> 2] = 0; + $1 = $3; + $3 = $2 & 4 | 24; + $1 = $1 - $3 | 0; + if ($1 >>> 0 < 32) { + break label$1 + } + $2 = $2 + $3 | 0; + while (1) { + HEAP32[$2 + 24 >> 2] = 0; + HEAP32[$2 + 28 >> 2] = 0; + HEAP32[$2 + 16 >> 2] = 0; + HEAP32[$2 + 20 >> 2] = 0; + HEAP32[$2 + 8 >> 2] = 0; + HEAP32[$2 + 12 >> 2] = 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + $2 = $2 + 32 | 0; + $1 = $1 + -32 | 0; + if ($1 >>> 0 > 31) { + continue + } + break; + }; + } + return $0; + } + + function memcpy($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0; + if ($2 >>> 0 >= 512) { + emscripten_memcpy_big($0 | 0, $1 | 0, $2 | 0) | 0; + return $0; + } + $4 = $0 + $2 | 0; + label$2 : { + if (!(($0 ^ $1) & 3)) { + label$4 : { + if (($2 | 0) < 1) { + $2 = $0; + break label$4; + } + if (!($0 & 3)) { + $2 = $0; + break label$4; + } + $2 = $0; + while (1) { + HEAP8[$2 | 0] = HEAPU8[$1 | 0]; + $1 = $1 + 1 | 0; + $2 = $2 + 1 | 0; + if ($2 >>> 0 >= $4 >>> 0) { + break label$4 + } + if ($2 & 3) { + continue + } + break; + }; + } + $3 = $4 & -4; + label$8 : { + if ($3 >>> 0 < 64) { + break label$8 + } + $5 = $3 + -64 | 0; + if ($2 >>> 0 > $5 >>> 0) { + break label$8 + } + while (1) { + HEAP32[$2 >> 2] = HEAP32[$1 >> 2]; + HEAP32[$2 + 4 >> 2] = HEAP32[$1 + 4 >> 2]; + HEAP32[$2 + 8 >> 2] = HEAP32[$1 + 8 >> 2]; + HEAP32[$2 + 12 >> 2] = HEAP32[$1 + 12 >> 2]; + HEAP32[$2 + 16 >> 2] = HEAP32[$1 + 16 >> 2]; + HEAP32[$2 + 20 >> 2] = HEAP32[$1 + 20 >> 2]; + HEAP32[$2 + 24 >> 2] = HEAP32[$1 + 24 >> 2]; + HEAP32[$2 + 28 >> 2] = HEAP32[$1 + 28 >> 2]; + HEAP32[$2 + 32 >> 2] = HEAP32[$1 + 32 >> 2]; + HEAP32[$2 + 36 >> 2] = HEAP32[$1 + 36 >> 2]; + HEAP32[$2 + 40 >> 2] = HEAP32[$1 + 40 >> 2]; + HEAP32[$2 + 44 >> 2] = HEAP32[$1 + 44 >> 2]; + HEAP32[$2 + 48 >> 2] = HEAP32[$1 + 48 >> 2]; + HEAP32[$2 + 52 >> 2] = HEAP32[$1 + 52 >> 2]; + HEAP32[$2 + 56 >> 2] = HEAP32[$1 + 56 >> 2]; + HEAP32[$2 + 60 >> 2] = HEAP32[$1 + 60 >> 2]; + $1 = $1 - -64 | 0; + $2 = $2 - -64 | 0; + if ($2 >>> 0 <= $5 >>> 0) { + continue + } + break; + }; + } + if ($2 >>> 0 >= $3 >>> 0) { + break label$2 + } + while (1) { + HEAP32[$2 >> 2] = HEAP32[$1 >> 2]; + $1 = $1 + 4 | 0; + $2 = $2 + 4 | 0; + if ($2 >>> 0 < $3 >>> 0) { + continue + } + break; + }; + break label$2; + } + if ($4 >>> 0 < 4) { + $2 = $0; + break label$2; + } + $3 = $4 + -4 | 0; + if ($3 >>> 0 < $0 >>> 0) { + $2 = $0; + break label$2; + } + $2 = $0; + while (1) { + HEAP8[$2 | 0] = HEAPU8[$1 | 0]; + HEAP8[$2 + 1 | 0] = HEAPU8[$1 + 1 | 0]; + HEAP8[$2 + 2 | 0] = HEAPU8[$1 + 2 | 0]; + HEAP8[$2 + 3 | 0] = HEAPU8[$1 + 3 | 0]; + $1 = $1 + 4 | 0; + $2 = $2 + 4 | 0; + if ($2 >>> 0 <= $3 >>> 0) { + continue + } + break; + }; + } + if ($2 >>> 0 < $4 >>> 0) { + while (1) { + HEAP8[$2 | 0] = HEAPU8[$1 | 0]; + $1 = $1 + 1 | 0; + $2 = $2 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + } + } + return $0; + } + + function dlmalloc($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $11 = global$0 - 16 | 0; + global$0 = $11; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + label$8 : { + label$9 : { + label$10 : { + label$11 : { + if ($0 >>> 0 <= 244) { + $6 = HEAP32[2897]; + $5 = $0 >>> 0 < 11 ? 16 : $0 + 11 & -8; + $0 = $5 >>> 3 | 0; + $1 = $6 >>> $0 | 0; + if ($1 & 3) { + $2 = $0 + (($1 ^ -1) & 1) | 0; + $5 = $2 << 3; + $1 = HEAP32[$5 + 11636 >> 2]; + $0 = $1 + 8 | 0; + $3 = HEAP32[$1 + 8 >> 2]; + $5 = $5 + 11628 | 0; + label$14 : { + if (($3 | 0) == ($5 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = __wasm_rotl_i32(-2, $2) & $6), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$14; + } + HEAP32[$3 + 12 >> 2] = $5; + HEAP32[$5 + 8 >> 2] = $3; + } + $2 = $2 << 3; + HEAP32[$1 + 4 >> 2] = $2 | 3; + $1 = $1 + $2 | 0; + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; + break label$1; + } + $7 = HEAP32[2899]; + if ($5 >>> 0 <= $7 >>> 0) { + break label$11 + } + if ($1) { + $2 = 2 << $0; + $0 = (0 - $2 | $2) & $1 << $0; + $0 = (0 - $0 & $0) + -1 | 0; + $1 = $0 >>> 12 & 16; + $2 = $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 5 & 8; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 2 & 4; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 2; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 1; + $2 = ($2 | $1) + ($0 >>> $1 | 0) | 0; + $3 = $2 << 3; + $1 = HEAP32[$3 + 11636 >> 2]; + $0 = HEAP32[$1 + 8 >> 2]; + $3 = $3 + 11628 | 0; + label$17 : { + if (($0 | 0) == ($3 | 0)) { + $6 = __wasm_rotl_i32(-2, $2) & $6; + HEAP32[2897] = $6; + break label$17; + } + HEAP32[$0 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $0; + } + $0 = $1 + 8 | 0; + HEAP32[$1 + 4 >> 2] = $5 | 3; + $4 = $1 + $5 | 0; + $2 = $2 << 3; + $3 = $2 - $5 | 0; + HEAP32[$4 + 4 >> 2] = $3 | 1; + HEAP32[$1 + $2 >> 2] = $3; + if ($7) { + $5 = $7 >>> 3 | 0; + $1 = ($5 << 3) + 11628 | 0; + $2 = HEAP32[2902]; + $5 = 1 << $5; + label$20 : { + if (!($5 & $6)) { + HEAP32[2897] = $5 | $6; + $5 = $1; + break label$20; + } + $5 = HEAP32[$1 + 8 >> 2]; + } + HEAP32[$1 + 8 >> 2] = $2; + HEAP32[$5 + 12 >> 2] = $2; + HEAP32[$2 + 12 >> 2] = $1; + HEAP32[$2 + 8 >> 2] = $5; + } + HEAP32[2902] = $4; + HEAP32[2899] = $3; + break label$1; + } + $10 = HEAP32[2898]; + if (!$10) { + break label$11 + } + $0 = ($10 & 0 - $10) + -1 | 0; + $1 = $0 >>> 12 & 16; + $2 = $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 5 & 8; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 2 & 4; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 2; + $2 = $2 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 1; + $1 = HEAP32[(($2 | $1) + ($0 >>> $1 | 0) << 2) + 11892 >> 2]; + $3 = (HEAP32[$1 + 4 >> 2] & -8) - $5 | 0; + $2 = $1; + while (1) { + label$23 : { + $0 = HEAP32[$2 + 16 >> 2]; + if (!$0) { + $0 = HEAP32[$2 + 20 >> 2]; + if (!$0) { + break label$23 + } + } + $4 = (HEAP32[$0 + 4 >> 2] & -8) - $5 | 0; + $2 = $4 >>> 0 < $3 >>> 0; + $3 = $2 ? $4 : $3; + $1 = $2 ? $0 : $1; + $2 = $0; + continue; + } + break; + }; + $9 = HEAP32[$1 + 24 >> 2]; + $4 = HEAP32[$1 + 12 >> 2]; + if (($4 | 0) != ($1 | 0)) { + $0 = HEAP32[$1 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $4; + HEAP32[$4 + 8 >> 2] = $0; + break label$2; + } + $2 = $1 + 20 | 0; + $0 = HEAP32[$2 >> 2]; + if (!$0) { + $0 = HEAP32[$1 + 16 >> 2]; + if (!$0) { + break label$10 + } + $2 = $1 + 16 | 0; + } + while (1) { + $8 = $2; + $4 = $0; + $2 = $0 + 20 | 0; + $0 = HEAP32[$2 >> 2]; + if ($0) { + continue + } + $2 = $4 + 16 | 0; + $0 = HEAP32[$4 + 16 >> 2]; + if ($0) { + continue + } + break; + }; + HEAP32[$8 >> 2] = 0; + break label$2; + } + $5 = -1; + if ($0 >>> 0 > 4294967231) { + break label$11 + } + $0 = $0 + 11 | 0; + $5 = $0 & -8; + $8 = HEAP32[2898]; + if (!$8) { + break label$11 + } + $2 = 0 - $5 | 0; + $0 = $0 >>> 8 | 0; + $7 = 0; + label$29 : { + if (!$0) { + break label$29 + } + $7 = 31; + if ($5 >>> 0 > 16777215) { + break label$29 + } + $3 = $0 + 1048320 >>> 16 & 8; + $1 = $0 << $3; + $0 = $1 + 520192 >>> 16 & 4; + $6 = $1 << $0; + $1 = $6 + 245760 >>> 16 & 2; + $0 = ($6 << $1 >>> 15 | 0) - ($1 | ($0 | $3)) | 0; + $7 = ($0 << 1 | $5 >>> $0 + 21 & 1) + 28 | 0; + } + $3 = HEAP32[($7 << 2) + 11892 >> 2]; + label$30 : { + label$31 : { + label$32 : { + if (!$3) { + $0 = 0; + break label$32; + } + $1 = $5 << (($7 | 0) == 31 ? 0 : 25 - ($7 >>> 1 | 0) | 0); + $0 = 0; + while (1) { + label$35 : { + $6 = (HEAP32[$3 + 4 >> 2] & -8) - $5 | 0; + if ($6 >>> 0 >= $2 >>> 0) { + break label$35 + } + $4 = $3; + $2 = $6; + if ($2) { + break label$35 + } + $2 = 0; + $0 = $3; + break label$31; + } + $6 = HEAP32[$3 + 20 >> 2]; + $3 = HEAP32[(($1 >>> 29 & 4) + $3 | 0) + 16 >> 2]; + $0 = $6 ? (($6 | 0) == ($3 | 0) ? $0 : $6) : $0; + $1 = $1 << (($3 | 0) != 0); + if ($3) { + continue + } + break; + }; + } + if (!($0 | $4)) { + $0 = 2 << $7; + $0 = (0 - $0 | $0) & $8; + if (!$0) { + break label$11 + } + $0 = ($0 & 0 - $0) + -1 | 0; + $1 = $0 >>> 12 & 16; + $3 = $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 5 & 8; + $3 = $3 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 2 & 4; + $3 = $3 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 2; + $3 = $3 | $1; + $0 = $0 >>> $1 | 0; + $1 = $0 >>> 1 & 1; + $0 = HEAP32[(($3 | $1) + ($0 >>> $1 | 0) << 2) + 11892 >> 2]; + } + if (!$0) { + break label$30 + } + } + while (1) { + $3 = (HEAP32[$0 + 4 >> 2] & -8) - $5 | 0; + $1 = $3 >>> 0 < $2 >>> 0; + $2 = $1 ? $3 : $2; + $4 = $1 ? $0 : $4; + $1 = HEAP32[$0 + 16 >> 2]; + if ($1) { + $0 = $1 + } else { + $0 = HEAP32[$0 + 20 >> 2] + } + if ($0) { + continue + } + break; + }; + } + if (!$4 | $2 >>> 0 >= HEAP32[2899] - $5 >>> 0) { + break label$11 + } + $7 = HEAP32[$4 + 24 >> 2]; + $1 = HEAP32[$4 + 12 >> 2]; + if (($4 | 0) != ($1 | 0)) { + $0 = HEAP32[$4 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $1; + HEAP32[$1 + 8 >> 2] = $0; + break label$3; + } + $3 = $4 + 20 | 0; + $0 = HEAP32[$3 >> 2]; + if (!$0) { + $0 = HEAP32[$4 + 16 >> 2]; + if (!$0) { + break label$9 + } + $3 = $4 + 16 | 0; + } + while (1) { + $6 = $3; + $1 = $0; + $3 = $0 + 20 | 0; + $0 = HEAP32[$3 >> 2]; + if ($0) { + continue + } + $3 = $1 + 16 | 0; + $0 = HEAP32[$1 + 16 >> 2]; + if ($0) { + continue + } + break; + }; + HEAP32[$6 >> 2] = 0; + break label$3; + } + $1 = HEAP32[2899]; + if ($1 >>> 0 >= $5 >>> 0) { + $0 = HEAP32[2902]; + $2 = $1 - $5 | 0; + label$45 : { + if ($2 >>> 0 >= 16) { + HEAP32[2899] = $2; + $3 = $0 + $5 | 0; + HEAP32[2902] = $3; + HEAP32[$3 + 4 >> 2] = $2 | 1; + HEAP32[$0 + $1 >> 2] = $2; + HEAP32[$0 + 4 >> 2] = $5 | 3; + break label$45; + } + HEAP32[2902] = 0; + HEAP32[2899] = 0; + HEAP32[$0 + 4 >> 2] = $1 | 3; + $1 = $0 + $1 | 0; + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; + } + $0 = $0 + 8 | 0; + break label$1; + } + $1 = HEAP32[2900]; + if ($1 >>> 0 > $5 >>> 0) { + $1 = $1 - $5 | 0; + HEAP32[2900] = $1; + $0 = HEAP32[2903]; + $2 = $0 + $5 | 0; + HEAP32[2903] = $2; + HEAP32[$2 + 4 >> 2] = $1 | 1; + HEAP32[$0 + 4 >> 2] = $5 | 3; + $0 = $0 + 8 | 0; + break label$1; + } + $0 = 0; + $4 = $5 + 47 | 0; + $3 = $4; + if (HEAP32[3015]) { + $2 = HEAP32[3017] + } else { + HEAP32[3018] = -1; + HEAP32[3019] = -1; + HEAP32[3016] = 4096; + HEAP32[3017] = 4096; + HEAP32[3015] = $11 + 12 & -16 ^ 1431655768; + HEAP32[3020] = 0; + HEAP32[3008] = 0; + $2 = 4096; + } + $6 = $3 + $2 | 0; + $8 = 0 - $2 | 0; + $2 = $6 & $8; + if ($2 >>> 0 <= $5 >>> 0) { + break label$1 + } + $3 = HEAP32[3007]; + if ($3) { + $7 = HEAP32[3005]; + $9 = $7 + $2 | 0; + if ($9 >>> 0 <= $7 >>> 0 | $9 >>> 0 > $3 >>> 0) { + break label$1 + } + } + if (HEAPU8[12032] & 4) { + break label$6 + } + label$51 : { + label$52 : { + $3 = HEAP32[2903]; + if ($3) { + $0 = 12036; + while (1) { + $7 = HEAP32[$0 >> 2]; + if ($7 + HEAP32[$0 + 4 >> 2] >>> 0 > $3 >>> 0 ? $7 >>> 0 <= $3 >>> 0 : 0) { + break label$52 + } + $0 = HEAP32[$0 + 8 >> 2]; + if ($0) { + continue + } + break; + }; + } + $1 = sbrk(0); + if (($1 | 0) == -1) { + break label$7 + } + $6 = $2; + $0 = HEAP32[3016]; + $3 = $0 + -1 | 0; + if ($3 & $1) { + $6 = ($2 - $1 | 0) + ($1 + $3 & 0 - $0) | 0 + } + if ($6 >>> 0 <= $5 >>> 0 | $6 >>> 0 > 2147483646) { + break label$7 + } + $0 = HEAP32[3007]; + if ($0) { + $3 = HEAP32[3005]; + $8 = $3 + $6 | 0; + if ($8 >>> 0 <= $3 >>> 0 | $8 >>> 0 > $0 >>> 0) { + break label$7 + } + } + $0 = sbrk($6); + if (($1 | 0) != ($0 | 0)) { + break label$51 + } + break label$5; + } + $6 = $8 & $6 - $1; + if ($6 >>> 0 > 2147483646) { + break label$7 + } + $1 = sbrk($6); + if (($1 | 0) == (HEAP32[$0 >> 2] + HEAP32[$0 + 4 >> 2] | 0)) { + break label$8 + } + $0 = $1; + } + if (!(($0 | 0) == -1 | $5 + 48 >>> 0 <= $6 >>> 0)) { + $1 = HEAP32[3017]; + $1 = $1 + ($4 - $6 | 0) & 0 - $1; + if ($1 >>> 0 > 2147483646) { + $1 = $0; + break label$5; + } + if ((sbrk($1) | 0) != -1) { + $6 = $1 + $6 | 0; + $1 = $0; + break label$5; + } + sbrk(0 - $6 | 0); + break label$7; + } + $1 = $0; + if (($0 | 0) != -1) { + break label$5 + } + break label$7; + } + $4 = 0; + break label$2; + } + $1 = 0; + break label$3; + } + if (($1 | 0) != -1) { + break label$5 + } + } + HEAP32[3008] = HEAP32[3008] | 4; + } + if ($2 >>> 0 > 2147483646) { + break label$4 + } + $1 = sbrk($2); + $0 = sbrk(0); + if ($1 >>> 0 >= $0 >>> 0 | ($1 | 0) == -1 | ($0 | 0) == -1) { + break label$4 + } + $6 = $0 - $1 | 0; + if ($6 >>> 0 <= $5 + 40 >>> 0) { + break label$4 + } + } + $0 = HEAP32[3005] + $6 | 0; + HEAP32[3005] = $0; + if ($0 >>> 0 > HEAPU32[3006]) { + HEAP32[3006] = $0 + } + label$62 : { + label$63 : { + label$64 : { + $3 = HEAP32[2903]; + if ($3) { + $0 = 12036; + while (1) { + $2 = HEAP32[$0 >> 2]; + $4 = HEAP32[$0 + 4 >> 2]; + if (($2 + $4 | 0) == ($1 | 0)) { + break label$64 + } + $0 = HEAP32[$0 + 8 >> 2]; + if ($0) { + continue + } + break; + }; + break label$63; + } + $0 = HEAP32[2901]; + if (!($1 >>> 0 >= $0 >>> 0 ? $0 : 0)) { + HEAP32[2901] = $1 + } + $0 = 0; + HEAP32[3010] = $6; + HEAP32[3009] = $1; + HEAP32[2905] = -1; + HEAP32[2906] = HEAP32[3015]; + HEAP32[3012] = 0; + while (1) { + $2 = $0 << 3; + $3 = $2 + 11628 | 0; + HEAP32[$2 + 11636 >> 2] = $3; + HEAP32[$2 + 11640 >> 2] = $3; + $0 = $0 + 1 | 0; + if (($0 | 0) != 32) { + continue + } + break; + }; + $0 = $6 + -40 | 0; + $2 = $1 + 8 & 7 ? -8 - $1 & 7 : 0; + $3 = $0 - $2 | 0; + HEAP32[2900] = $3; + $2 = $1 + $2 | 0; + HEAP32[2903] = $2; + HEAP32[$2 + 4 >> 2] = $3 | 1; + HEAP32[($0 + $1 | 0) + 4 >> 2] = 40; + HEAP32[2904] = HEAP32[3019]; + break label$62; + } + if (HEAPU8[$0 + 12 | 0] & 8 | $1 >>> 0 <= $3 >>> 0 | $2 >>> 0 > $3 >>> 0) { + break label$63 + } + HEAP32[$0 + 4 >> 2] = $4 + $6; + $0 = $3 + 8 & 7 ? -8 - $3 & 7 : 0; + $1 = $0 + $3 | 0; + HEAP32[2903] = $1; + $2 = HEAP32[2900] + $6 | 0; + $0 = $2 - $0 | 0; + HEAP32[2900] = $0; + HEAP32[$1 + 4 >> 2] = $0 | 1; + HEAP32[($2 + $3 | 0) + 4 >> 2] = 40; + HEAP32[2904] = HEAP32[3019]; + break label$62; + } + $0 = HEAP32[2901]; + if ($1 >>> 0 < $0 >>> 0) { + HEAP32[2901] = $1; + $0 = 0; + } + $2 = $1 + $6 | 0; + $0 = 12036; + label$70 : { + label$71 : { + label$72 : { + label$73 : { + label$74 : { + label$75 : { + while (1) { + if (($2 | 0) != HEAP32[$0 >> 2]) { + $0 = HEAP32[$0 + 8 >> 2]; + if ($0) { + continue + } + break label$75; + } + break; + }; + if (!(HEAPU8[$0 + 12 | 0] & 8)) { + break label$74 + } + } + $0 = 12036; + while (1) { + $2 = HEAP32[$0 >> 2]; + if ($2 >>> 0 <= $3 >>> 0) { + $4 = $2 + HEAP32[$0 + 4 >> 2] | 0; + if ($4 >>> 0 > $3 >>> 0) { + break label$73 + } + } + $0 = HEAP32[$0 + 8 >> 2]; + continue; + }; + } + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + $6; + $7 = ($1 + 8 & 7 ? -8 - $1 & 7 : 0) + $1 | 0; + HEAP32[$7 + 4 >> 2] = $5 | 3; + $1 = $2 + ($2 + 8 & 7 ? -8 - $2 & 7 : 0) | 0; + $0 = ($1 - $7 | 0) - $5 | 0; + $4 = $5 + $7 | 0; + if (($1 | 0) == ($3 | 0)) { + HEAP32[2903] = $4; + $0 = HEAP32[2900] + $0 | 0; + HEAP32[2900] = $0; + HEAP32[$4 + 4 >> 2] = $0 | 1; + break label$71; + } + if (HEAP32[2902] == ($1 | 0)) { + HEAP32[2902] = $4; + $0 = HEAP32[2899] + $0 | 0; + HEAP32[2899] = $0; + HEAP32[$4 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $4 >> 2] = $0; + break label$71; + } + $2 = HEAP32[$1 + 4 >> 2]; + if (($2 & 3) == 1) { + $9 = $2 & -8; + label$83 : { + if ($2 >>> 0 <= 255) { + $3 = HEAP32[$1 + 8 >> 2]; + $5 = $2 >>> 3 | 0; + $2 = HEAP32[$1 + 12 >> 2]; + if (($2 | 0) == ($3 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $5)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$83; + } + HEAP32[$3 + 12 >> 2] = $2; + HEAP32[$2 + 8 >> 2] = $3; + break label$83; + } + $8 = HEAP32[$1 + 24 >> 2]; + $6 = HEAP32[$1 + 12 >> 2]; + label$86 : { + if (($6 | 0) != ($1 | 0)) { + $2 = HEAP32[$1 + 8 >> 2]; + HEAP32[$2 + 12 >> 2] = $6; + HEAP32[$6 + 8 >> 2] = $2; + break label$86; + } + label$89 : { + $3 = $1 + 20 | 0; + $5 = HEAP32[$3 >> 2]; + if ($5) { + break label$89 + } + $3 = $1 + 16 | 0; + $5 = HEAP32[$3 >> 2]; + if ($5) { + break label$89 + } + $6 = 0; + break label$86; + } + while (1) { + $2 = $3; + $6 = $5; + $3 = $5 + 20 | 0; + $5 = HEAP32[$3 >> 2]; + if ($5) { + continue + } + $3 = $6 + 16 | 0; + $5 = HEAP32[$6 + 16 >> 2]; + if ($5) { + continue + } + break; + }; + HEAP32[$2 >> 2] = 0; + } + if (!$8) { + break label$83 + } + $2 = HEAP32[$1 + 28 >> 2]; + $3 = ($2 << 2) + 11892 | 0; + label$91 : { + if (HEAP32[$3 >> 2] == ($1 | 0)) { + HEAP32[$3 >> 2] = $6; + if ($6) { + break label$91 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$83; + } + HEAP32[$8 + (HEAP32[$8 + 16 >> 2] == ($1 | 0) ? 16 : 20) >> 2] = $6; + if (!$6) { + break label$83 + } + } + HEAP32[$6 + 24 >> 2] = $8; + $2 = HEAP32[$1 + 16 >> 2]; + if ($2) { + HEAP32[$6 + 16 >> 2] = $2; + HEAP32[$2 + 24 >> 2] = $6; + } + $2 = HEAP32[$1 + 20 >> 2]; + if (!$2) { + break label$83 + } + HEAP32[$6 + 20 >> 2] = $2; + HEAP32[$2 + 24 >> 2] = $6; + } + $1 = $1 + $9 | 0; + $0 = $0 + $9 | 0; + } + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] & -2; + HEAP32[$4 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $4 >> 2] = $0; + if ($0 >>> 0 <= 255) { + $1 = $0 >>> 3 | 0; + $0 = ($1 << 3) + 11628 | 0; + $2 = HEAP32[2897]; + $1 = 1 << $1; + label$95 : { + if (!($2 & $1)) { + HEAP32[2897] = $1 | $2; + $1 = $0; + break label$95; + } + $1 = HEAP32[$0 + 8 >> 2]; + } + HEAP32[$0 + 8 >> 2] = $4; + HEAP32[$1 + 12 >> 2] = $4; + HEAP32[$4 + 12 >> 2] = $0; + HEAP32[$4 + 8 >> 2] = $1; + break label$71; + } + $6 = $4; + $1 = $0 >>> 8 | 0; + $2 = 0; + label$97 : { + if (!$1) { + break label$97 + } + $2 = 31; + if ($0 >>> 0 > 16777215) { + break label$97 + } + $3 = $1 + 1048320 >>> 16 & 8; + $2 = $1 << $3; + $1 = $2 + 520192 >>> 16 & 4; + $5 = $2 << $1; + $2 = $5 + 245760 >>> 16 & 2; + $1 = ($5 << $2 >>> 15 | 0) - ($2 | ($1 | $3)) | 0; + $2 = ($1 << 1 | $0 >>> $1 + 21 & 1) + 28 | 0; + } + $1 = $2; + HEAP32[$6 + 28 >> 2] = $1; + HEAP32[$4 + 16 >> 2] = 0; + HEAP32[$4 + 20 >> 2] = 0; + $2 = ($1 << 2) + 11892 | 0; + $3 = HEAP32[2898]; + $5 = 1 << $1; + label$98 : { + if (!($3 & $5)) { + HEAP32[2898] = $3 | $5; + HEAP32[$2 >> 2] = $4; + break label$98; + } + $3 = $0 << (($1 | 0) == 31 ? 0 : 25 - ($1 >>> 1 | 0) | 0); + $1 = HEAP32[$2 >> 2]; + while (1) { + $2 = $1; + if ((HEAP32[$1 + 4 >> 2] & -8) == ($0 | 0)) { + break label$72 + } + $1 = $3 >>> 29 | 0; + $3 = $3 << 1; + $5 = ($2 + ($1 & 4) | 0) + 16 | 0; + $1 = HEAP32[$5 >> 2]; + if ($1) { + continue + } + break; + }; + HEAP32[$5 >> 2] = $4; + } + HEAP32[$4 + 24 >> 2] = $2; + HEAP32[$4 + 12 >> 2] = $4; + HEAP32[$4 + 8 >> 2] = $4; + break label$71; + } + $0 = $6 + -40 | 0; + $2 = $1 + 8 & 7 ? -8 - $1 & 7 : 0; + $8 = $0 - $2 | 0; + HEAP32[2900] = $8; + $2 = $1 + $2 | 0; + HEAP32[2903] = $2; + HEAP32[$2 + 4 >> 2] = $8 | 1; + HEAP32[($0 + $1 | 0) + 4 >> 2] = 40; + HEAP32[2904] = HEAP32[3019]; + $0 = ($4 + ($4 + -39 & 7 ? 39 - $4 & 7 : 0) | 0) + -47 | 0; + $2 = $0 >>> 0 < $3 + 16 >>> 0 ? $3 : $0; + HEAP32[$2 + 4 >> 2] = 27; + $0 = HEAP32[3012]; + HEAP32[$2 + 16 >> 2] = HEAP32[3011]; + HEAP32[$2 + 20 >> 2] = $0; + $0 = HEAP32[3010]; + HEAP32[$2 + 8 >> 2] = HEAP32[3009]; + HEAP32[$2 + 12 >> 2] = $0; + HEAP32[3011] = $2 + 8; + HEAP32[3010] = $6; + HEAP32[3009] = $1; + HEAP32[3012] = 0; + $0 = $2 + 24 | 0; + while (1) { + HEAP32[$0 + 4 >> 2] = 7; + $1 = $0 + 8 | 0; + $0 = $0 + 4 | 0; + if ($4 >>> 0 > $1 >>> 0) { + continue + } + break; + }; + if (($2 | 0) == ($3 | 0)) { + break label$62 + } + HEAP32[$2 + 4 >> 2] = HEAP32[$2 + 4 >> 2] & -2; + $6 = $2 - $3 | 0; + HEAP32[$3 + 4 >> 2] = $6 | 1; + HEAP32[$2 >> 2] = $6; + if ($6 >>> 0 <= 255) { + $1 = $6 >>> 3 | 0; + $0 = ($1 << 3) + 11628 | 0; + $2 = HEAP32[2897]; + $1 = 1 << $1; + label$103 : { + if (!($2 & $1)) { + HEAP32[2897] = $1 | $2; + $1 = $0; + break label$103; + } + $1 = HEAP32[$0 + 8 >> 2]; + } + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$1 + 12 >> 2] = $3; + HEAP32[$3 + 12 >> 2] = $0; + HEAP32[$3 + 8 >> 2] = $1; + break label$62; + } + HEAP32[$3 + 16 >> 2] = 0; + HEAP32[$3 + 20 >> 2] = 0; + $7 = $3; + $0 = $6 >>> 8 | 0; + $1 = 0; + label$105 : { + if (!$0) { + break label$105 + } + $1 = 31; + if ($6 >>> 0 > 16777215) { + break label$105 + } + $2 = $0 + 1048320 >>> 16 & 8; + $1 = $0 << $2; + $0 = $1 + 520192 >>> 16 & 4; + $4 = $1 << $0; + $1 = $4 + 245760 >>> 16 & 2; + $0 = ($4 << $1 >>> 15 | 0) - ($1 | ($0 | $2)) | 0; + $1 = ($0 << 1 | $6 >>> $0 + 21 & 1) + 28 | 0; + } + $0 = $1; + HEAP32[$7 + 28 >> 2] = $0; + $1 = ($0 << 2) + 11892 | 0; + $2 = HEAP32[2898]; + $4 = 1 << $0; + label$106 : { + if (!($2 & $4)) { + HEAP32[2898] = $2 | $4; + HEAP32[$1 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $1; + break label$106; + } + $0 = $6 << (($0 | 0) == 31 ? 0 : 25 - ($0 >>> 1 | 0) | 0); + $1 = HEAP32[$1 >> 2]; + while (1) { + $2 = $1; + if (($6 | 0) == (HEAP32[$1 + 4 >> 2] & -8)) { + break label$70 + } + $1 = $0 >>> 29 | 0; + $0 = $0 << 1; + $4 = ($2 + ($1 & 4) | 0) + 16 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + continue + } + break; + }; + HEAP32[$4 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $2; + } + HEAP32[$3 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $3; + break label$62; + } + $0 = HEAP32[$2 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $4; + HEAP32[$2 + 8 >> 2] = $4; + HEAP32[$4 + 24 >> 2] = 0; + HEAP32[$4 + 12 >> 2] = $2; + HEAP32[$4 + 8 >> 2] = $0; + } + $0 = $7 + 8 | 0; + break label$1; + } + $0 = HEAP32[$2 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $3; + HEAP32[$2 + 8 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = 0; + HEAP32[$3 + 12 >> 2] = $2; + HEAP32[$3 + 8 >> 2] = $0; + } + $0 = HEAP32[2900]; + if ($0 >>> 0 <= $5 >>> 0) { + break label$4 + } + $1 = $0 - $5 | 0; + HEAP32[2900] = $1; + $0 = HEAP32[2903]; + $2 = $0 + $5 | 0; + HEAP32[2903] = $2; + HEAP32[$2 + 4 >> 2] = $1 | 1; + HEAP32[$0 + 4 >> 2] = $5 | 3; + $0 = $0 + 8 | 0; + break label$1; + } + HEAP32[2896] = 48; + $0 = 0; + break label$1; + } + label$109 : { + if (!$7) { + break label$109 + } + $0 = HEAP32[$4 + 28 >> 2]; + $3 = ($0 << 2) + 11892 | 0; + label$110 : { + if (HEAP32[$3 >> 2] == ($4 | 0)) { + HEAP32[$3 >> 2] = $1; + if ($1) { + break label$110 + } + $8 = __wasm_rotl_i32(-2, $0) & $8; + HEAP32[2898] = $8; + break label$109; + } + HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($4 | 0) ? 16 : 20) >> 2] = $1; + if (!$1) { + break label$109 + } + } + HEAP32[$1 + 24 >> 2] = $7; + $0 = HEAP32[$4 + 16 >> 2]; + if ($0) { + HEAP32[$1 + 16 >> 2] = $0; + HEAP32[$0 + 24 >> 2] = $1; + } + $0 = HEAP32[$4 + 20 >> 2]; + if (!$0) { + break label$109 + } + HEAP32[$1 + 20 >> 2] = $0; + HEAP32[$0 + 24 >> 2] = $1; + } + label$113 : { + if ($2 >>> 0 <= 15) { + $0 = $2 + $5 | 0; + HEAP32[$4 + 4 >> 2] = $0 | 3; + $0 = $0 + $4 | 0; + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] | 1; + break label$113; + } + HEAP32[$4 + 4 >> 2] = $5 | 3; + $1 = $4 + $5 | 0; + HEAP32[$1 + 4 >> 2] = $2 | 1; + HEAP32[$1 + $2 >> 2] = $2; + if ($2 >>> 0 <= 255) { + $2 = $2 >>> 3 | 0; + $0 = ($2 << 3) + 11628 | 0; + $3 = HEAP32[2897]; + $2 = 1 << $2; + label$116 : { + if (!($3 & $2)) { + HEAP32[2897] = $2 | $3; + $2 = $0; + break label$116; + } + $2 = HEAP32[$0 + 8 >> 2]; + } + HEAP32[$0 + 8 >> 2] = $1; + HEAP32[$2 + 12 >> 2] = $1; + HEAP32[$1 + 12 >> 2] = $0; + HEAP32[$1 + 8 >> 2] = $2; + break label$113; + } + $7 = $1; + $0 = $2 >>> 8 | 0; + $3 = 0; + label$118 : { + if (!$0) { + break label$118 + } + $3 = 31; + if ($2 >>> 0 > 16777215) { + break label$118 + } + $5 = $0 + 1048320 >>> 16 & 8; + $3 = $0 << $5; + $0 = $3 + 520192 >>> 16 & 4; + $6 = $3 << $0; + $3 = $6 + 245760 >>> 16 & 2; + $0 = ($6 << $3 >>> 15 | 0) - ($3 | ($0 | $5)) | 0; + $3 = ($0 << 1 | $2 >>> $0 + 21 & 1) + 28 | 0; + } + $0 = $3; + HEAP32[$7 + 28 >> 2] = $0; + HEAP32[$1 + 16 >> 2] = 0; + HEAP32[$1 + 20 >> 2] = 0; + $3 = ($0 << 2) + 11892 | 0; + label$119 : { + $5 = 1 << $0; + label$120 : { + if (!($5 & $8)) { + HEAP32[2898] = $5 | $8; + HEAP32[$3 >> 2] = $1; + break label$120; + } + $0 = $2 << (($0 | 0) == 31 ? 0 : 25 - ($0 >>> 1 | 0) | 0); + $5 = HEAP32[$3 >> 2]; + while (1) { + $3 = $5; + if ((HEAP32[$3 + 4 >> 2] & -8) == ($2 | 0)) { + break label$119 + } + $5 = $0 >>> 29 | 0; + $0 = $0 << 1; + $6 = ($3 + ($5 & 4) | 0) + 16 | 0; + $5 = HEAP32[$6 >> 2]; + if ($5) { + continue + } + break; + }; + HEAP32[$6 >> 2] = $1; + } + HEAP32[$1 + 24 >> 2] = $3; + HEAP32[$1 + 12 >> 2] = $1; + HEAP32[$1 + 8 >> 2] = $1; + break label$113; + } + $0 = HEAP32[$3 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $1; + HEAP32[$3 + 8 >> 2] = $1; + HEAP32[$1 + 24 >> 2] = 0; + HEAP32[$1 + 12 >> 2] = $3; + HEAP32[$1 + 8 >> 2] = $0; + } + $0 = $4 + 8 | 0; + break label$1; + } + label$123 : { + if (!$9) { + break label$123 + } + $0 = HEAP32[$1 + 28 >> 2]; + $2 = ($0 << 2) + 11892 | 0; + label$124 : { + if (HEAP32[$2 >> 2] == ($1 | 0)) { + HEAP32[$2 >> 2] = $4; + if ($4) { + break label$124 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = __wasm_rotl_i32(-2, $0) & $10), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$123; + } + HEAP32[(HEAP32[$9 + 16 >> 2] == ($1 | 0) ? 16 : 20) + $9 >> 2] = $4; + if (!$4) { + break label$123 + } + } + HEAP32[$4 + 24 >> 2] = $9; + $0 = HEAP32[$1 + 16 >> 2]; + if ($0) { + HEAP32[$4 + 16 >> 2] = $0; + HEAP32[$0 + 24 >> 2] = $4; + } + $0 = HEAP32[$1 + 20 >> 2]; + if (!$0) { + break label$123 + } + HEAP32[$4 + 20 >> 2] = $0; + HEAP32[$0 + 24 >> 2] = $4; + } + label$127 : { + if ($3 >>> 0 <= 15) { + $0 = $3 + $5 | 0; + HEAP32[$1 + 4 >> 2] = $0 | 3; + $0 = $0 + $1 | 0; + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] | 1; + break label$127; + } + HEAP32[$1 + 4 >> 2] = $5 | 3; + $5 = $1 + $5 | 0; + HEAP32[$5 + 4 >> 2] = $3 | 1; + HEAP32[$3 + $5 >> 2] = $3; + if ($7) { + $4 = $7 >>> 3 | 0; + $0 = ($4 << 3) + 11628 | 0; + $2 = HEAP32[2902]; + $4 = 1 << $4; + label$130 : { + if (!($4 & $6)) { + HEAP32[2897] = $4 | $6; + $6 = $0; + break label$130; + } + $6 = HEAP32[$0 + 8 >> 2]; + } + HEAP32[$0 + 8 >> 2] = $2; + HEAP32[$6 + 12 >> 2] = $2; + HEAP32[$2 + 12 >> 2] = $0; + HEAP32[$2 + 8 >> 2] = $6; + } + HEAP32[2902] = $5; + HEAP32[2899] = $3; + } + $0 = $1 + 8 | 0; + } + global$0 = $11 + 16 | 0; + return $0 | 0; + } + + function dlfree($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + label$1 : { + if (!$0) { + break label$1 + } + $3 = $0 + -8 | 0; + $2 = HEAP32[$0 + -4 >> 2]; + $0 = $2 & -8; + $5 = $3 + $0 | 0; + label$2 : { + if ($2 & 1) { + break label$2 + } + if (!($2 & 3)) { + break label$1 + } + $2 = HEAP32[$3 >> 2]; + $3 = $3 - $2 | 0; + if ($3 >>> 0 < HEAPU32[2901]) { + break label$1 + } + $0 = $0 + $2 | 0; + if (HEAP32[2902] != ($3 | 0)) { + if ($2 >>> 0 <= 255) { + $4 = HEAP32[$3 + 8 >> 2]; + $2 = $2 >>> 3 | 0; + $1 = HEAP32[$3 + 12 >> 2]; + if (($1 | 0) == ($4 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$2; + } + HEAP32[$4 + 12 >> 2] = $1; + HEAP32[$1 + 8 >> 2] = $4; + break label$2; + } + $7 = HEAP32[$3 + 24 >> 2]; + $2 = HEAP32[$3 + 12 >> 2]; + label$6 : { + if (($2 | 0) != ($3 | 0)) { + $1 = HEAP32[$3 + 8 >> 2]; + HEAP32[$1 + 12 >> 2] = $2; + HEAP32[$2 + 8 >> 2] = $1; + break label$6; + } + label$9 : { + $4 = $3 + 20 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + break label$9 + } + $4 = $3 + 16 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + break label$9 + } + $2 = 0; + break label$6; + } + while (1) { + $6 = $4; + $2 = $1; + $4 = $2 + 20 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + continue + } + $4 = $2 + 16 | 0; + $1 = HEAP32[$2 + 16 >> 2]; + if ($1) { + continue + } + break; + }; + HEAP32[$6 >> 2] = 0; + } + if (!$7) { + break label$2 + } + $4 = HEAP32[$3 + 28 >> 2]; + $1 = ($4 << 2) + 11892 | 0; + label$11 : { + if (HEAP32[$1 >> 2] == ($3 | 0)) { + HEAP32[$1 >> 2] = $2; + if ($2) { + break label$11 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$2; + } + HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($3 | 0) ? 16 : 20) >> 2] = $2; + if (!$2) { + break label$2 + } + } + HEAP32[$2 + 24 >> 2] = $7; + $1 = HEAP32[$3 + 16 >> 2]; + if ($1) { + HEAP32[$2 + 16 >> 2] = $1; + HEAP32[$1 + 24 >> 2] = $2; + } + $1 = HEAP32[$3 + 20 >> 2]; + if (!$1) { + break label$2 + } + HEAP32[$2 + 20 >> 2] = $1; + HEAP32[$1 + 24 >> 2] = $2; + break label$2; + } + $2 = HEAP32[$5 + 4 >> 2]; + if (($2 & 3) != 3) { + break label$2 + } + HEAP32[2899] = $0; + HEAP32[$5 + 4 >> 2] = $2 & -2; + HEAP32[$3 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $3 >> 2] = $0; + return; + } + if ($5 >>> 0 <= $3 >>> 0) { + break label$1 + } + $2 = HEAP32[$5 + 4 >> 2]; + if (!($2 & 1)) { + break label$1 + } + label$14 : { + if (!($2 & 2)) { + if (($5 | 0) == HEAP32[2903]) { + HEAP32[2903] = $3; + $0 = HEAP32[2900] + $0 | 0; + HEAP32[2900] = $0; + HEAP32[$3 + 4 >> 2] = $0 | 1; + if (HEAP32[2902] != ($3 | 0)) { + break label$1 + } + HEAP32[2899] = 0; + HEAP32[2902] = 0; + return; + } + if (($5 | 0) == HEAP32[2902]) { + HEAP32[2902] = $3; + $0 = HEAP32[2899] + $0 | 0; + HEAP32[2899] = $0; + HEAP32[$3 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $3 >> 2] = $0; + return; + } + $0 = ($2 & -8) + $0 | 0; + label$18 : { + if ($2 >>> 0 <= 255) { + $1 = HEAP32[$5 + 8 >> 2]; + $2 = $2 >>> 3 | 0; + $4 = HEAP32[$5 + 12 >> 2]; + if (($1 | 0) == ($4 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$18; + } + HEAP32[$1 + 12 >> 2] = $4; + HEAP32[$4 + 8 >> 2] = $1; + break label$18; + } + $7 = HEAP32[$5 + 24 >> 2]; + $2 = HEAP32[$5 + 12 >> 2]; + label$23 : { + if (($5 | 0) != ($2 | 0)) { + $1 = HEAP32[$5 + 8 >> 2]; + HEAP32[$1 + 12 >> 2] = $2; + HEAP32[$2 + 8 >> 2] = $1; + break label$23; + } + label$26 : { + $4 = $5 + 20 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + break label$26 + } + $4 = $5 + 16 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + break label$26 + } + $2 = 0; + break label$23; + } + while (1) { + $6 = $4; + $2 = $1; + $4 = $2 + 20 | 0; + $1 = HEAP32[$4 >> 2]; + if ($1) { + continue + } + $4 = $2 + 16 | 0; + $1 = HEAP32[$2 + 16 >> 2]; + if ($1) { + continue + } + break; + }; + HEAP32[$6 >> 2] = 0; + } + if (!$7) { + break label$18 + } + $4 = HEAP32[$5 + 28 >> 2]; + $1 = ($4 << 2) + 11892 | 0; + label$28 : { + if (($5 | 0) == HEAP32[$1 >> 2]) { + HEAP32[$1 >> 2] = $2; + if ($2) { + break label$28 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$18; + } + HEAP32[$7 + (($5 | 0) == HEAP32[$7 + 16 >> 2] ? 16 : 20) >> 2] = $2; + if (!$2) { + break label$18 + } + } + HEAP32[$2 + 24 >> 2] = $7; + $1 = HEAP32[$5 + 16 >> 2]; + if ($1) { + HEAP32[$2 + 16 >> 2] = $1; + HEAP32[$1 + 24 >> 2] = $2; + } + $1 = HEAP32[$5 + 20 >> 2]; + if (!$1) { + break label$18 + } + HEAP32[$2 + 20 >> 2] = $1; + HEAP32[$1 + 24 >> 2] = $2; + } + HEAP32[$3 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $3 >> 2] = $0; + if (HEAP32[2902] != ($3 | 0)) { + break label$14 + } + HEAP32[2899] = $0; + return; + } + HEAP32[$5 + 4 >> 2] = $2 & -2; + HEAP32[$3 + 4 >> 2] = $0 | 1; + HEAP32[$0 + $3 >> 2] = $0; + } + if ($0 >>> 0 <= 255) { + $0 = $0 >>> 3 | 0; + $2 = ($0 << 3) + 11628 | 0; + $1 = HEAP32[2897]; + $0 = 1 << $0; + label$32 : { + if (!($1 & $0)) { + HEAP32[2897] = $0 | $1; + $0 = $2; + break label$32; + } + $0 = HEAP32[$2 + 8 >> 2]; + } + HEAP32[$2 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $3; + HEAP32[$3 + 12 >> 2] = $2; + HEAP32[$3 + 8 >> 2] = $0; + return; + } + HEAP32[$3 + 16 >> 2] = 0; + HEAP32[$3 + 20 >> 2] = 0; + $5 = $3; + $4 = $0 >>> 8 | 0; + $1 = 0; + label$34 : { + if (!$4) { + break label$34 + } + $1 = 31; + if ($0 >>> 0 > 16777215) { + break label$34 + } + $2 = $4; + $4 = $4 + 1048320 >>> 16 & 8; + $1 = $2 << $4; + $7 = $1 + 520192 >>> 16 & 4; + $1 = $1 << $7; + $6 = $1 + 245760 >>> 16 & 2; + $1 = ($1 << $6 >>> 15 | 0) - ($6 | ($4 | $7)) | 0; + $1 = ($1 << 1 | $0 >>> $1 + 21 & 1) + 28 | 0; + } + HEAP32[$5 + 28 >> 2] = $1; + $6 = ($1 << 2) + 11892 | 0; + label$35 : { + label$36 : { + $4 = HEAP32[2898]; + $2 = 1 << $1; + label$37 : { + if (!($4 & $2)) { + HEAP32[2898] = $2 | $4; + HEAP32[$6 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $6; + break label$37; + } + $4 = $0 << (($1 | 0) == 31 ? 0 : 25 - ($1 >>> 1 | 0) | 0); + $2 = HEAP32[$6 >> 2]; + while (1) { + $1 = $2; + if ((HEAP32[$2 + 4 >> 2] & -8) == ($0 | 0)) { + break label$36 + } + $2 = $4 >>> 29 | 0; + $4 = $4 << 1; + $6 = ($1 + ($2 & 4) | 0) + 16 | 0; + $2 = HEAP32[$6 >> 2]; + if ($2) { + continue + } + break; + }; + HEAP32[$6 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $1; + } + HEAP32[$3 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $3; + break label$35; + } + $0 = HEAP32[$1 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $3; + HEAP32[$1 + 8 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = 0; + HEAP32[$3 + 12 >> 2] = $1; + HEAP32[$3 + 8 >> 2] = $0; + } + $0 = HEAP32[2905] + -1 | 0; + HEAP32[2905] = $0; + if ($0) { + break label$1 + } + $3 = 12044; + while (1) { + $0 = HEAP32[$3 >> 2]; + $3 = $0 + 8 | 0; + if ($0) { + continue + } + break; + }; + HEAP32[2905] = -1; + } + } + + function dlcalloc($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $2 = 0; + label$2 : { + if (!$0) { + break label$2 + } + $3 = __wasm_i64_mul($0, 0, $1, 0); + $4 = i64toi32_i32$HIGH_BITS; + $2 = $3; + if (($0 | $1) >>> 0 < 65536) { + break label$2 + } + $2 = $4 ? -1 : $3; + } + $1 = $2; + $0 = dlmalloc($1); + if (!(!$0 | !(HEAPU8[$0 + -4 | 0] & 3))) { + memset($0, $1) + } + return $0; + } + + function dlrealloc($0, $1) { + var $2 = 0, $3 = 0; + if (!$0) { + return dlmalloc($1) + } + if ($1 >>> 0 >= 4294967232) { + HEAP32[2896] = 48; + return 0; + } + $2 = try_realloc_chunk($0 + -8 | 0, $1 >>> 0 < 11 ? 16 : $1 + 11 & -8); + if ($2) { + return $2 + 8 | 0 + } + $2 = dlmalloc($1); + if (!$2) { + return 0 + } + $3 = HEAP32[$0 + -4 >> 2]; + $3 = ($3 & 3 ? -4 : -8) + ($3 & -8) | 0; + memcpy($2, $0, $3 >>> 0 < $1 >>> 0 ? $3 : $1); + dlfree($0); + return $2; + } + + function try_realloc_chunk($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $7 = HEAP32[$0 + 4 >> 2]; + $2 = $7 & 3; + $3 = $7 & -8; + $5 = $3 + $0 | 0; + label$2 : { + if (!$2) { + $2 = 0; + if ($1 >>> 0 < 256) { + break label$2 + } + if ($3 >>> 0 >= $1 + 4 >>> 0) { + $2 = $0; + if ($3 - $1 >>> 0 <= HEAP32[3017] << 1 >>> 0) { + break label$2 + } + } + return 0; + } + label$5 : { + if ($3 >>> 0 >= $1 >>> 0) { + $2 = $3 - $1 | 0; + if ($2 >>> 0 < 16) { + break label$5 + } + HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; + $1 = $0 + $1 | 0; + HEAP32[$1 + 4 >> 2] = $2 | 3; + HEAP32[$5 + 4 >> 2] = HEAP32[$5 + 4 >> 2] | 1; + dispose_chunk($1, $2); + break label$5; + } + $2 = 0; + if (($5 | 0) == HEAP32[2903]) { + $4 = $3 + HEAP32[2900] | 0; + if ($4 >>> 0 <= $1 >>> 0) { + break label$2 + } + HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; + $2 = $0 + $1 | 0; + $1 = $4 - $1 | 0; + HEAP32[$2 + 4 >> 2] = $1 | 1; + HEAP32[2900] = $1; + HEAP32[2903] = $2; + break label$5; + } + if (($5 | 0) == HEAP32[2902]) { + $4 = $3 + HEAP32[2899] | 0; + if ($4 >>> 0 < $1 >>> 0) { + break label$2 + } + $2 = $4 - $1 | 0; + label$9 : { + if ($2 >>> 0 >= 16) { + HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; + $1 = $0 + $1 | 0; + HEAP32[$1 + 4 >> 2] = $2 | 1; + $4 = $0 + $4 | 0; + HEAP32[$4 >> 2] = $2; + HEAP32[$4 + 4 >> 2] = HEAP32[$4 + 4 >> 2] & -2; + break label$9; + } + HEAP32[$0 + 4 >> 2] = $4 | $7 & 1 | 2; + $1 = $0 + $4 | 0; + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; + $2 = 0; + $1 = 0; + } + HEAP32[2902] = $1; + HEAP32[2899] = $2; + break label$5; + } + $6 = HEAP32[$5 + 4 >> 2]; + if ($6 & 2) { + break label$2 + } + $8 = $3 + ($6 & -8) | 0; + if ($8 >>> 0 < $1 >>> 0) { + break label$2 + } + $10 = $8 - $1 | 0; + label$11 : { + if ($6 >>> 0 <= 255) { + $2 = $6 >>> 3 | 0; + $6 = HEAP32[$5 + 8 >> 2]; + $4 = HEAP32[$5 + 12 >> 2]; + if (($6 | 0) == ($4 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$11; + } + HEAP32[$6 + 12 >> 2] = $4; + HEAP32[$4 + 8 >> 2] = $6; + break label$11; + } + $9 = HEAP32[$5 + 24 >> 2]; + $3 = HEAP32[$5 + 12 >> 2]; + label$14 : { + if (($5 | 0) != ($3 | 0)) { + $2 = HEAP32[$5 + 8 >> 2]; + HEAP32[$2 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $2; + break label$14; + } + label$17 : { + $2 = $5 + 20 | 0; + $6 = HEAP32[$2 >> 2]; + if ($6) { + break label$17 + } + $2 = $5 + 16 | 0; + $6 = HEAP32[$2 >> 2]; + if ($6) { + break label$17 + } + $3 = 0; + break label$14; + } + while (1) { + $4 = $2; + $3 = $6; + $2 = $3 + 20 | 0; + $6 = HEAP32[$2 >> 2]; + if ($6) { + continue + } + $2 = $3 + 16 | 0; + $6 = HEAP32[$3 + 16 >> 2]; + if ($6) { + continue + } + break; + }; + HEAP32[$4 >> 2] = 0; + } + if (!$9) { + break label$11 + } + $4 = HEAP32[$5 + 28 >> 2]; + $2 = ($4 << 2) + 11892 | 0; + label$19 : { + if (($5 | 0) == HEAP32[$2 >> 2]) { + HEAP32[$2 >> 2] = $3; + if ($3) { + break label$19 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$11; + } + HEAP32[(($5 | 0) == HEAP32[$9 + 16 >> 2] ? 16 : 20) + $9 >> 2] = $3; + if (!$3) { + break label$11 + } + } + HEAP32[$3 + 24 >> 2] = $9; + $2 = HEAP32[$5 + 16 >> 2]; + if ($2) { + HEAP32[$3 + 16 >> 2] = $2; + HEAP32[$2 + 24 >> 2] = $3; + } + $2 = HEAP32[$5 + 20 >> 2]; + if (!$2) { + break label$11 + } + HEAP32[$3 + 20 >> 2] = $2; + HEAP32[$2 + 24 >> 2] = $3; + } + if ($10 >>> 0 <= 15) { + HEAP32[$0 + 4 >> 2] = $7 & 1 | $8 | 2; + $1 = $0 + $8 | 0; + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; + break label$5; + } + HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; + $2 = $0 + $1 | 0; + HEAP32[$2 + 4 >> 2] = $10 | 3; + $1 = $0 + $8 | 0; + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; + dispose_chunk($2, $10); + } + $2 = $0; + } + return $2; + } + + function dispose_chunk($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $5 = $0 + $1 | 0; + label$1 : { + label$2 : { + $2 = HEAP32[$0 + 4 >> 2]; + if ($2 & 1) { + break label$2 + } + if (!($2 & 3)) { + break label$1 + } + $2 = HEAP32[$0 >> 2]; + $1 = $2 + $1 | 0; + $0 = $0 - $2 | 0; + if (($0 | 0) != HEAP32[2902]) { + if ($2 >>> 0 <= 255) { + $4 = $2 >>> 3 | 0; + $2 = HEAP32[$0 + 8 >> 2]; + $3 = HEAP32[$0 + 12 >> 2]; + if (($3 | 0) == ($2 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$2; + } + HEAP32[$2 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $2; + break label$2; + } + $7 = HEAP32[$0 + 24 >> 2]; + $2 = HEAP32[$0 + 12 >> 2]; + label$6 : { + if (($2 | 0) != ($0 | 0)) { + $3 = HEAP32[$0 + 8 >> 2]; + HEAP32[$3 + 12 >> 2] = $2; + HEAP32[$2 + 8 >> 2] = $3; + break label$6; + } + label$9 : { + $3 = $0 + 20 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + break label$9 + } + $3 = $0 + 16 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + break label$9 + } + $2 = 0; + break label$6; + } + while (1) { + $6 = $3; + $2 = $4; + $3 = $2 + 20 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + continue + } + $3 = $2 + 16 | 0; + $4 = HEAP32[$2 + 16 >> 2]; + if ($4) { + continue + } + break; + }; + HEAP32[$6 >> 2] = 0; + } + if (!$7) { + break label$2 + } + $3 = HEAP32[$0 + 28 >> 2]; + $4 = ($3 << 2) + 11892 | 0; + label$11 : { + if (HEAP32[$4 >> 2] == ($0 | 0)) { + HEAP32[$4 >> 2] = $2; + if ($2) { + break label$11 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $3)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$2; + } + HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($0 | 0) ? 16 : 20) >> 2] = $2; + if (!$2) { + break label$2 + } + } + HEAP32[$2 + 24 >> 2] = $7; + $3 = HEAP32[$0 + 16 >> 2]; + if ($3) { + HEAP32[$2 + 16 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $2; + } + $3 = HEAP32[$0 + 20 >> 2]; + if (!$3) { + break label$2 + } + HEAP32[$2 + 20 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $2; + break label$2; + } + $2 = HEAP32[$5 + 4 >> 2]; + if (($2 & 3) != 3) { + break label$2 + } + HEAP32[2899] = $1; + HEAP32[$5 + 4 >> 2] = $2 & -2; + HEAP32[$0 + 4 >> 2] = $1 | 1; + HEAP32[$5 >> 2] = $1; + return; + } + $2 = HEAP32[$5 + 4 >> 2]; + label$14 : { + if (!($2 & 2)) { + if (($5 | 0) == HEAP32[2903]) { + HEAP32[2903] = $0; + $1 = HEAP32[2900] + $1 | 0; + HEAP32[2900] = $1; + HEAP32[$0 + 4 >> 2] = $1 | 1; + if (HEAP32[2902] != ($0 | 0)) { + break label$1 + } + HEAP32[2899] = 0; + HEAP32[2902] = 0; + return; + } + if (($5 | 0) == HEAP32[2902]) { + HEAP32[2902] = $0; + $1 = HEAP32[2899] + $1 | 0; + HEAP32[2899] = $1; + HEAP32[$0 + 4 >> 2] = $1 | 1; + HEAP32[$0 + $1 >> 2] = $1; + return; + } + $1 = ($2 & -8) + $1 | 0; + label$18 : { + if ($2 >>> 0 <= 255) { + $4 = $2 >>> 3 | 0; + $2 = HEAP32[$5 + 8 >> 2]; + $3 = HEAP32[$5 + 12 >> 2]; + if (($2 | 0) == ($3 | 0)) { + (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$18; + } + HEAP32[$2 + 12 >> 2] = $3; + HEAP32[$3 + 8 >> 2] = $2; + break label$18; + } + $7 = HEAP32[$5 + 24 >> 2]; + $2 = HEAP32[$5 + 12 >> 2]; + label$21 : { + if (($5 | 0) != ($2 | 0)) { + $3 = HEAP32[$5 + 8 >> 2]; + HEAP32[$3 + 12 >> 2] = $2; + HEAP32[$2 + 8 >> 2] = $3; + break label$21; + } + label$24 : { + $3 = $5 + 20 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + break label$24 + } + $3 = $5 + 16 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + break label$24 + } + $2 = 0; + break label$21; + } + while (1) { + $6 = $3; + $2 = $4; + $3 = $2 + 20 | 0; + $4 = HEAP32[$3 >> 2]; + if ($4) { + continue + } + $3 = $2 + 16 | 0; + $4 = HEAP32[$2 + 16 >> 2]; + if ($4) { + continue + } + break; + }; + HEAP32[$6 >> 2] = 0; + } + if (!$7) { + break label$18 + } + $3 = HEAP32[$5 + 28 >> 2]; + $4 = ($3 << 2) + 11892 | 0; + label$26 : { + if (($5 | 0) == HEAP32[$4 >> 2]) { + HEAP32[$4 >> 2] = $2; + if ($2) { + break label$26 + } + (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $3)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + break label$18; + } + HEAP32[$7 + (($5 | 0) == HEAP32[$7 + 16 >> 2] ? 16 : 20) >> 2] = $2; + if (!$2) { + break label$18 + } + } + HEAP32[$2 + 24 >> 2] = $7; + $3 = HEAP32[$5 + 16 >> 2]; + if ($3) { + HEAP32[$2 + 16 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $2; + } + $3 = HEAP32[$5 + 20 >> 2]; + if (!$3) { + break label$18 + } + HEAP32[$2 + 20 >> 2] = $3; + HEAP32[$3 + 24 >> 2] = $2; + } + HEAP32[$0 + 4 >> 2] = $1 | 1; + HEAP32[$0 + $1 >> 2] = $1; + if (HEAP32[2902] != ($0 | 0)) { + break label$14 + } + HEAP32[2899] = $1; + return; + } + HEAP32[$5 + 4 >> 2] = $2 & -2; + HEAP32[$0 + 4 >> 2] = $1 | 1; + HEAP32[$0 + $1 >> 2] = $1; + } + if ($1 >>> 0 <= 255) { + $2 = $1 >>> 3 | 0; + $1 = ($2 << 3) + 11628 | 0; + $3 = HEAP32[2897]; + $2 = 1 << $2; + label$30 : { + if (!($3 & $2)) { + HEAP32[2897] = $2 | $3; + $2 = $1; + break label$30; + } + $2 = HEAP32[$1 + 8 >> 2]; + } + HEAP32[$1 + 8 >> 2] = $0; + HEAP32[$2 + 12 >> 2] = $0; + HEAP32[$0 + 12 >> 2] = $1; + HEAP32[$0 + 8 >> 2] = $2; + return; + } + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + $3 = $0; + $4 = $1 >>> 8 | 0; + $2 = 0; + label$32 : { + if (!$4) { + break label$32 + } + $2 = 31; + if ($1 >>> 0 > 16777215) { + break label$32 + } + $6 = $4 + 1048320 >>> 16 & 8; + $4 = $4 << $6; + $2 = $4 + 520192 >>> 16 & 4; + $5 = $4 << $2; + $4 = $5 + 245760 >>> 16 & 2; + $2 = ($5 << $4 >>> 15 | 0) - ($4 | ($2 | $6)) | 0; + $2 = ($2 << 1 | $1 >>> $2 + 21 & 1) + 28 | 0; + } + HEAP32[$3 + 28 >> 2] = $2; + $4 = ($2 << 2) + 11892 | 0; + label$33 : { + $3 = HEAP32[2898]; + $6 = 1 << $2; + label$34 : { + if (!($3 & $6)) { + HEAP32[2898] = $3 | $6; + HEAP32[$4 >> 2] = $0; + break label$34; + } + $3 = $1 << (($2 | 0) == 31 ? 0 : 25 - ($2 >>> 1 | 0) | 0); + $2 = HEAP32[$4 >> 2]; + while (1) { + $4 = $2; + if ((HEAP32[$2 + 4 >> 2] & -8) == ($1 | 0)) { + break label$33 + } + $2 = $3 >>> 29 | 0; + $3 = $3 << 1; + $6 = ($4 + ($2 & 4) | 0) + 16 | 0; + $2 = HEAP32[$6 >> 2]; + if ($2) { + continue + } + break; + }; + HEAP32[$6 >> 2] = $0; + } + HEAP32[$0 + 24 >> 2] = $4; + HEAP32[$0 + 12 >> 2] = $0; + HEAP32[$0 + 8 >> 2] = $0; + return; + } + $1 = HEAP32[$4 + 8 >> 2]; + HEAP32[$1 + 12 >> 2] = $0; + HEAP32[$4 + 8 >> 2] = $0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = $4; + HEAP32[$0 + 8 >> 2] = $1; + } + } + + function memchr($0, $1) { + var $2 = 0; + $2 = ($1 | 0) != 0; + label$1 : { + label$2 : { + label$3 : { + if (!$1 | !($0 & 3)) { + break label$3 + } + while (1) { + if (HEAPU8[$0 | 0] == 79) { + break label$2 + } + $0 = $0 + 1 | 0; + $1 = $1 + -1 | 0; + $2 = ($1 | 0) != 0; + if (!$1) { + break label$3 + } + if ($0 & 3) { + continue + } + break; + }; + } + if (!$2) { + break label$1 + } + } + label$5 : { + if (HEAPU8[$0 | 0] == 79 | $1 >>> 0 < 4) { + break label$5 + } + while (1) { + $2 = HEAP32[$0 >> 2] ^ 1330597711; + if (($2 ^ -1) & $2 + -16843009 & -2139062144) { + break label$5 + } + $0 = $0 + 4 | 0; + $1 = $1 + -4 | 0; + if ($1 >>> 0 > 3) { + continue + } + break; + }; + } + if (!$1) { + break label$1 + } + while (1) { + if (HEAPU8[$0 | 0] == 79) { + return $0 + } + $0 = $0 + 1 | 0; + $1 = $1 + -1 | 0; + if ($1) { + continue + } + break; + }; + } + return 0; + } + + function frexp($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + wasm2js_scratch_store_f64(+$0); + $2 = wasm2js_scratch_load_i32(1) | 0; + $3 = wasm2js_scratch_load_i32(0) | 0; + $4 = $2; + $2 = $2 >>> 20 & 2047; + if (($2 | 0) != 2047) { + if (!$2) { + $2 = $1; + if ($0 == 0.0) { + $1 = 0 + } else { + $0 = frexp($0 * 18446744073709551615.0, $1); + $1 = HEAP32[$1 >> 2] + -64 | 0; + } + HEAP32[$2 >> 2] = $1; + return $0; + } + HEAP32[$1 >> 2] = $2 + -1022; + wasm2js_scratch_store_i32(0, $3 | 0); + wasm2js_scratch_store_i32(1, $4 & -2146435073 | 1071644672); + $0 = +wasm2js_scratch_load_f64(); + } + return $0; + } + + function __ashlti3($0, $1, $2, $3, $4, $5) { + var $6 = 0, $7 = 0, $8 = 0, $9 = 0; + label$1 : { + if ($5 & 64) { + $3 = $1; + $4 = $5 + -64 | 0; + $1 = $4 & 31; + if (32 <= ($4 & 63) >>> 0) { + $4 = $3 << $1; + $3 = 0; + } else { + $4 = (1 << $1) - 1 & $3 >>> 32 - $1 | $2 << $1; + $3 = $3 << $1; + } + $1 = 0; + $2 = 0; + break label$1; + } + if (!$5) { + break label$1 + } + $6 = $3; + $8 = $5; + $3 = $5 & 31; + if (32 <= ($5 & 63) >>> 0) { + $7 = $6 << $3; + $9 = 0; + } else { + $7 = (1 << $3) - 1 & $6 >>> 32 - $3 | $4 << $3; + $9 = $6 << $3; + } + $3 = $2; + $6 = $1; + $5 = 64 - $5 | 0; + $4 = $5 & 31; + if (32 <= ($5 & 63) >>> 0) { + $5 = 0; + $3 = $3 >>> $4 | 0; + } else { + $5 = $3 >>> $4 | 0; + $3 = ((1 << $4) - 1 & $3) << 32 - $4 | $6 >>> $4; + } + $3 = $9 | $3; + $4 = $5 | $7; + $5 = $1; + $1 = $8 & 31; + if (32 <= ($8 & 63) >>> 0) { + $7 = $5 << $1; + $1 = 0; + } else { + $7 = (1 << $1) - 1 & $5 >>> 32 - $1 | $2 << $1; + $1 = $5 << $1; + } + $2 = $7; + } + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $4; + } + + function __lshrti3($0, $1, $2, $3, $4, $5) { + var $6 = 0, $7 = 0, $8 = 0, $9 = 0; + label$1 : { + if ($5 & 64) { + $2 = $5 + -64 | 0; + $1 = $2 & 31; + if (32 <= ($2 & 63) >>> 0) { + $2 = 0; + $1 = $4 >>> $1 | 0; + } else { + $2 = $4 >>> $1 | 0; + $1 = ((1 << $1) - 1 & $4) << 32 - $1 | $3 >>> $1; + } + $3 = 0; + $4 = 0; + break label$1; + } + if (!$5) { + break label$1 + } + $7 = $4; + $8 = $3; + $9 = 64 - $5 | 0; + $6 = $9 & 31; + if (32 <= ($9 & 63) >>> 0) { + $7 = $8 << $6; + $9 = 0; + } else { + $7 = (1 << $6) - 1 & $8 >>> 32 - $6 | $7 << $6; + $9 = $8 << $6; + } + $8 = $1; + $6 = $5; + $1 = $6 & 31; + if (32 <= ($6 & 63) >>> 0) { + $6 = 0; + $1 = $2 >>> $1 | 0; + } else { + $6 = $2 >>> $1 | 0; + $1 = ((1 << $1) - 1 & $2) << 32 - $1 | $8 >>> $1; + } + $1 = $9 | $1; + $2 = $6 | $7; + $6 = $3; + $3 = $5 & 31; + if (32 <= ($5 & 63) >>> 0) { + $7 = 0; + $3 = $4 >>> $3 | 0; + } else { + $7 = $4 >>> $3 | 0; + $3 = ((1 << $3) - 1 & $4) << 32 - $3 | $6 >>> $3; + } + $4 = $7; + } + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $4; + } + + function __trunctfdf2($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; + $6 = global$0 - 32 | 0; + global$0 = $6; + $4 = $3 & 2147483647; + $8 = $4; + $4 = $4 + -1006698496 | 0; + $7 = $2; + $5 = $2; + if ($2 >>> 0 < 0) { + $4 = $4 + 1 | 0 + } + $9 = $5; + $5 = $4; + $4 = $8 + -1140785152 | 0; + $10 = $7; + if ($7 >>> 0 < 0) { + $4 = $4 + 1 | 0 + } + label$1 : { + if (($4 | 0) == ($5 | 0) & $9 >>> 0 < $10 >>> 0 | $5 >>> 0 < $4 >>> 0) { + $4 = $3 << 4 | $2 >>> 28; + $2 = $2 << 4 | $1 >>> 28; + $1 = $1 & 268435455; + $7 = $1; + if (($1 | 0) == 134217728 & $0 >>> 0 >= 1 | $1 >>> 0 > 134217728) { + $4 = $4 + 1073741824 | 0; + $0 = $2 + 1 | 0; + if ($0 >>> 0 < 1) { + $4 = $4 + 1 | 0 + } + $5 = $0; + break label$1; + } + $5 = $2; + $4 = $4 - (($2 >>> 0 < 0) + -1073741824 | 0) | 0; + if ($0 | $7 ^ 134217728) { + break label$1 + } + $0 = $5 + ($5 & 1) | 0; + if ($0 >>> 0 < $5 >>> 0) { + $4 = $4 + 1 | 0 + } + $5 = $0; + break label$1; + } + if (!(!$7 & ($8 | 0) == 2147418112 ? !($0 | $1) : ($8 | 0) == 2147418112 & $7 >>> 0 < 0 | $8 >>> 0 < 2147418112)) { + $4 = $3 << 4 | $2 >>> 28; + $5 = $2 << 4 | $1 >>> 28; + $4 = $4 & 524287 | 2146959360; + break label$1; + } + $5 = 0; + $4 = 2146435072; + if ($8 >>> 0 > 1140785151) { + break label$1 + } + $4 = 0; + $7 = $8 >>> 16 | 0; + if ($7 >>> 0 < 15249) { + break label$1 + } + $4 = $3 & 65535 | 65536; + __ashlti3($6 + 16 | 0, $0, $1, $2, $4, $7 + -15233 | 0); + __lshrti3($6, $0, $1, $2, $4, 15361 - $7 | 0); + $2 = HEAP32[$6 + 4 >> 2]; + $0 = HEAP32[$6 + 8 >> 2]; + $4 = HEAP32[$6 + 12 >> 2] << 4 | $0 >>> 28; + $5 = $0 << 4 | $2 >>> 28; + $0 = $2 & 268435455; + $2 = $0; + $1 = HEAP32[$6 >> 2] | ((HEAP32[$6 + 16 >> 2] | HEAP32[$6 + 24 >> 2]) != 0 | (HEAP32[$6 + 20 >> 2] | HEAP32[$6 + 28 >> 2]) != 0); + if (($0 | 0) == 134217728 & $1 >>> 0 >= 1 | $0 >>> 0 > 134217728) { + $0 = $5 + 1 | 0; + if ($0 >>> 0 < 1) { + $4 = $4 + 1 | 0 + } + $5 = $0; + break label$1; + } + if ($1 | $2 ^ 134217728) { + break label$1 + } + $0 = $5 + ($5 & 1) | 0; + if ($0 >>> 0 < $5 >>> 0) { + $4 = $4 + 1 | 0 + } + $5 = $0; + } + global$0 = $6 + 32 | 0; + wasm2js_scratch_store_i32(0, $5 | 0); + wasm2js_scratch_store_i32(1, $3 & -2147483648 | $4); + return +wasm2js_scratch_load_f64(); + } + + function FLAC__crc8($0, $1) { + var $2 = 0; + if ($1) { + while (1) { + $2 = HEAPU8[(HEAPU8[$0 | 0] ^ $2) + 1024 | 0]; + $0 = $0 + 1 | 0; + $1 = $1 + -1 | 0; + if ($1) { + continue + } + break; + } + } + return $2; + } + + function FLAC__crc16($0, $1) { + var $2 = 0, $3 = 0; + if ($1 >>> 0 > 7) { + while (1) { + $3 = $2; + $2 = HEAPU8[$0 | 0] | HEAPU8[$0 + 1 | 0] << 8; + $2 = $3 ^ ($2 << 8 & 16711680 | $2 << 24) >>> 16; + $2 = HEAPU16[(HEAPU8[$0 + 7 | 0] << 1) + 1280 >> 1] ^ (HEAPU16[((HEAPU8[$0 + 6 | 0] << 1) + 1280 | 0) + 512 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 5 | 0] << 1) + 2304 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 4 | 0] << 1) + 2816 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 3 | 0] << 1) + 3328 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 2 | 0] << 1) + 3840 >> 1] ^ (HEAPU16[(($2 & 255) << 1) + 4352 >> 1] ^ HEAPU16[($2 >>> 7 & 510) + 4864 >> 1])))))); + $0 = $0 + 8 | 0; + $1 = $1 + -8 | 0; + if ($1 >>> 0 > 7) { + continue + } + break; + } + } + if ($1) { + while (1) { + $2 = HEAPU16[((HEAPU8[$0 | 0] ^ ($2 & 65280) >>> 8) << 1) + 1280 >> 1] ^ $2 << 8; + $0 = $0 + 1 | 0; + $1 = $1 + -1 | 0; + if ($1) { + continue + } + break; + } + } + return $2 & 65535; + } + + function FLAC__crc16_update_words32($0, $1, $2) { + var $3 = 0; + if ($1 >>> 0 >= 2) { + while (1) { + $3 = $2; + $2 = HEAP32[$0 >> 2]; + $3 = $3 ^ $2 >>> 16; + $3 = HEAPU16[(($3 & 255) << 1) + 4352 >> 1] ^ HEAPU16[($3 >>> 7 & 510) + 4864 >> 1] ^ HEAPU16[($2 >>> 7 & 510) + 3840 >> 1] ^ HEAPU16[(($2 & 255) << 1) + 3328 >> 1]; + $2 = HEAP32[$0 + 4 >> 2]; + $2 = $3 ^ HEAPU16[($2 >>> 23 & 510) + 2816 >> 1] ^ HEAPU16[($2 >>> 15 & 510) + 2304 >> 1] ^ HEAPU16[(($2 >>> 7 & 510) + 1280 | 0) + 512 >> 1] ^ HEAPU16[(($2 & 255) << 1) + 1280 >> 1]; + $0 = $0 + 8 | 0; + $1 = $1 + -2 | 0; + if ($1 >>> 0 > 1) { + continue + } + break; + } + } + if ($1) { + $0 = HEAP32[$0 >> 2]; + $1 = $0 >>> 16 ^ $2; + $2 = HEAPU16[(($1 & 255) << 1) + 2304 >> 1] ^ HEAPU16[($1 >>> 7 & 510) + 2816 >> 1] ^ HEAPU16[(($0 >>> 7 & 510) + 1280 | 0) + 512 >> 1] ^ HEAPU16[(($0 & 255) << 1) + 1280 >> 1]; + } + return $2 & 65535; + } + + function memmove($0, $1, $2) { + var $3 = 0; + label$1 : { + if (($0 | 0) == ($1 | 0)) { + break label$1 + } + if (($1 - $0 | 0) - $2 >>> 0 <= 0 - ($2 << 1) >>> 0) { + memcpy($0, $1, $2); + return; + } + $3 = ($0 ^ $1) & 3; + label$3 : { + label$4 : { + if ($0 >>> 0 < $1 >>> 0) { + if ($3) { + break label$3 + } + if (!($0 & 3)) { + break label$4 + } + while (1) { + if (!$2) { + break label$1 + } + HEAP8[$0 | 0] = HEAPU8[$1 | 0]; + $1 = $1 + 1 | 0; + $2 = $2 + -1 | 0; + $0 = $0 + 1 | 0; + if ($0 & 3) { + continue + } + break; + }; + break label$4; + } + label$9 : { + if ($3) { + break label$9 + } + if ($0 + $2 & 3) { + while (1) { + if (!$2) { + break label$1 + } + $2 = $2 + -1 | 0; + $3 = $2 + $0 | 0; + HEAP8[$3 | 0] = HEAPU8[$1 + $2 | 0]; + if ($3 & 3) { + continue + } + break; + } + } + if ($2 >>> 0 <= 3) { + break label$9 + } + while (1) { + $2 = $2 + -4 | 0; + HEAP32[$2 + $0 >> 2] = HEAP32[$1 + $2 >> 2]; + if ($2 >>> 0 > 3) { + continue + } + break; + }; + } + if (!$2) { + break label$1 + } + while (1) { + $2 = $2 + -1 | 0; + HEAP8[$2 + $0 | 0] = HEAPU8[$1 + $2 | 0]; + if ($2) { + continue + } + break; + }; + break label$1; + } + if ($2 >>> 0 <= 3) { + break label$3 + } + while (1) { + HEAP32[$0 >> 2] = HEAP32[$1 >> 2]; + $1 = $1 + 4 | 0; + $0 = $0 + 4 | 0; + $2 = $2 + -4 | 0; + if ($2 >>> 0 > 3) { + continue + } + break; + }; + } + if (!$2) { + break label$1 + } + while (1) { + HEAP8[$0 | 0] = HEAPU8[$1 | 0]; + $0 = $0 + 1 | 0; + $1 = $1 + 1 | 0; + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break; + }; + } + } + + function FLAC__bitreader_delete($0) { + var $1 = 0; + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + dlfree($0); + } + + function FLAC__bitreader_free($0) { + var $1 = 0; + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 + 36 >> 2] = 0; + HEAP32[$0 + 40 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + } + + function FLAC__bitreader_init($0, $1) { + var $2 = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 2048; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + $2 = dlmalloc(8192); + HEAP32[$0 >> 2] = $2; + if (!$2) { + return 0 + } + HEAP32[$0 + 40 >> 2] = $1; + HEAP32[$0 + 36 >> 2] = 7; + return 1; + } + + function FLAC__bitreader_get_read_crc16($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $5 = HEAP32[$0 + 16 >> 2]; + $2 = HEAP32[$0 + 28 >> 2]; + label$1 : { + if ($5 >>> 0 <= $2 >>> 0) { + $4 = $2; + break label$1; + } + $1 = HEAP32[$0 + 32 >> 2]; + if (!$1) { + $4 = $2; + break label$1; + } + $4 = $2 + 1 | 0; + HEAP32[$0 + 28 >> 2] = $4; + $3 = HEAP32[$0 + 24 >> 2]; + if ($1 >>> 0 <= 31) { + $2 = HEAP32[HEAP32[$0 >> 2] + ($2 << 2) >> 2]; + while (1) { + $3 = HEAPU16[(($2 >>> 24 - $1 & 255 ^ $3 >>> 8) << 1) + 1280 >> 1] ^ $3 << 8 & 65280; + $7 = $1 >>> 0 < 24; + $6 = $1 + 8 | 0; + $1 = $6; + if ($7) { + continue + } + break; + }; + HEAP32[$0 + 32 >> 2] = $6; + } + HEAP32[$0 + 32 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = $3; + } + $1 = FLAC__crc16_update_words32(HEAP32[$0 >> 2] + ($4 << 2) | 0, $5 - $4 | 0, HEAPU16[$0 + 24 >> 1]); + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = $1; + $2 = HEAP32[$0 + 20 >> 2]; + label$6 : { + if (!$2) { + break label$6 + } + $3 = HEAP32[$0 + 32 >> 2]; + if ($3 >>> 0 >= $2 >>> 0) { + break label$6 + } + $4 = HEAP32[HEAP32[$0 >> 2] + (HEAP32[$0 + 16 >> 2] << 2) >> 2]; + while (1) { + $1 = HEAPU16[(($4 >>> 24 - $3 & 255 ^ $1 >>> 8) << 1) + 1280 >> 1] ^ $1 << 8 & 65280; + $3 = $3 + 8 | 0; + if ($3 >>> 0 < $2 >>> 0) { + continue + } + break; + }; + HEAP32[$0 + 32 >> 2] = $3; + HEAP32[$0 + 24 >> 2] = $1; + } + return $1; + } + + function FLAC__bitreader_is_consumed_byte_aligned($0) { + return !(HEAPU8[$0 + 20 | 0] & 7); + } + + function FLAC__bitreader_bits_left_for_byte_alignment($0) { + return 8 - (HEAP32[$0 + 20 >> 2] & 7) | 0; + } + + function FLAC__bitreader_read_raw_uint32($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0; + label$1 : { + if ($2) { + label$4 : { + while (1) { + $5 = HEAP32[$0 + 8 >> 2]; + $4 = HEAP32[$0 + 16 >> 2]; + $3 = HEAP32[$0 + 20 >> 2]; + if ((($5 - $4 << 5) + (HEAP32[$0 + 12 >> 2] << 3) | 0) - $3 >>> 0 >= $2 >>> 0) { + break label$4 + } + if (bitreader_read_from_client_($0)) { + continue + } + break; + }; + return 0; + } + if ($5 >>> 0 > $4 >>> 0) { + if ($3) { + $5 = HEAP32[$0 >> 2]; + $4 = HEAP32[$5 + ($4 << 2) >> 2] & -1 >>> $3; + $3 = 32 - $3 | 0; + if ($3 >>> 0 > $2 >>> 0) { + HEAP32[$1 >> 2] = $4 >>> $3 - $2; + HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; + break label$1; + } + HEAP32[$1 >> 2] = $4; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; + $2 = $2 - $3 | 0; + if (!$2) { + break label$1 + } + $3 = HEAP32[$1 >> 2] << $2; + HEAP32[$1 >> 2] = $3; + HEAP32[$1 >> 2] = $3 | HEAP32[(HEAP32[$0 + 16 >> 2] << 2) + $5 >> 2] >>> 32 - $2; + HEAP32[$0 + 20 >> 2] = $2; + return 1; + } + $3 = HEAP32[HEAP32[$0 >> 2] + ($4 << 2) >> 2]; + if ($2 >>> 0 <= 31) { + HEAP32[$1 >> 2] = $3 >>> 32 - $2; + HEAP32[$0 + 20 >> 2] = $2; + break label$1; + } + HEAP32[$1 >> 2] = $3; + HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; + return 1; + } + $4 = HEAP32[HEAP32[$0 >> 2] + ($4 << 2) >> 2]; + if ($3) { + HEAP32[$1 >> 2] = ($4 & -1 >>> $3) >>> 32 - ($2 + $3 | 0); + HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; + break label$1; + } + HEAP32[$1 >> 2] = $4 >>> 32 - $2; + HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; + break label$1; + } + HEAP32[$1 >> 2] = 0; + } + return 1; + } + + function bitreader_read_from_client_($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + $6 = global$0 - 16 | 0; + global$0 = $6; + $5 = HEAP32[$0 + 16 >> 2]; + label$1 : { + if (!$5) { + $2 = HEAP32[$0 + 8 >> 2]; + break label$1; + } + $1 = HEAP32[$0 + 28 >> 2]; + label$3 : { + if ($5 >>> 0 <= $1 >>> 0) { + $3 = $1; + break label$3; + } + $2 = HEAP32[$0 + 32 >> 2]; + if (!$2) { + $3 = $1; + break label$3; + } + $3 = $1 + 1 | 0; + HEAP32[$0 + 28 >> 2] = $3; + $4 = HEAP32[$0 + 24 >> 2]; + if ($2 >>> 0 <= 31) { + $1 = HEAP32[HEAP32[$0 >> 2] + ($1 << 2) >> 2]; + while (1) { + $4 = HEAPU16[(($1 >>> 24 - $2 & 255 ^ $4 >>> 8) << 1) + 1280 >> 1] ^ $4 << 8 & 65280; + $7 = $2 >>> 0 < 24; + $8 = $2 + 8 | 0; + $2 = $8; + if ($7) { + continue + } + break; + }; + HEAP32[$0 + 32 >> 2] = $8; + } + HEAP32[$0 + 32 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = $4; + } + $1 = FLAC__crc16_update_words32(HEAP32[$0 >> 2] + ($3 << 2) | 0, $5 - $3 | 0, HEAPU16[$0 + 24 >> 1]); + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = $1; + $3 = HEAP32[$0 >> 2]; + $1 = HEAP32[$0 + 16 >> 2]; + memmove($3, $3 + ($1 << 2) | 0, (HEAP32[$0 + 8 >> 2] - $1 | 0) + (HEAP32[$0 + 12 >> 2] != 0) << 2); + HEAP32[$0 + 16 >> 2] = 0; + $2 = HEAP32[$0 + 8 >> 2] - $1 | 0; + HEAP32[$0 + 8 >> 2] = $2; + } + $1 = HEAP32[$0 + 12 >> 2]; + $3 = (HEAP32[$0 + 4 >> 2] - $2 << 2) - $1 | 0; + HEAP32[$6 + 12 >> 2] = $3; + $4 = 0; + label$8 : { + if (!$3) { + break label$8 + } + $3 = HEAP32[$0 >> 2] + ($2 << 2) | 0; + $2 = $3 + $1 | 0; + if ($1) { + $1 = HEAP32[$3 >> 2]; + HEAP32[$3 >> 2] = $1 << 24 | $1 << 8 & 16711680 | ($1 >>> 8 & 65280 | $1 >>> 24); + } + if (!FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($2, $6 + 12 | 0, HEAP32[$0 + 40 >> 2])) { + break label$8 + } + $5 = HEAP32[$6 + 12 >> 2]; + $2 = HEAP32[$0 + 12 >> 2]; + $4 = HEAP32[$0 + 8 >> 2]; + $1 = $4 << 2; + $3 = ($5 + ($2 + $1 | 0) | 0) + 3 >>> 2 | 0; + $8 = $0; + if ($4 >>> 0 < $3 >>> 0) { + $2 = HEAP32[$0 >> 2]; + while (1) { + $7 = $2 + ($4 << 2) | 0; + $1 = HEAP32[$7 >> 2]; + HEAP32[$7 >> 2] = $1 << 8 & 16711680 | $1 << 24 | ($1 >>> 8 & 65280 | $1 >>> 24); + $4 = $4 + 1 | 0; + if (($3 | 0) != ($4 | 0)) { + continue + } + break; + }; + $2 = HEAP32[$0 + 12 >> 2]; + $1 = HEAP32[$0 + 8 >> 2] << 2; + } + $1 = $1 + ($2 + $5 | 0) | 0; + HEAP32[$8 + 12 >> 2] = $1 & 3; + HEAP32[$0 + 8 >> 2] = $1 >>> 2; + $4 = 1; + } + global$0 = $6 + 16 | 0; + return $4; + } + + function FLAC__bitreader_read_raw_int32($0, $1, $2) { + var $3 = 0, $4 = 0; + $3 = global$0 - 16 | 0; + global$0 = $3; + $4 = 0; + label$1 : { + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, $2)) { + break label$1 + } + $0 = 1 << $2 + -1; + HEAP32[$1 >> 2] = ($0 ^ HEAP32[$3 + 12 >> 2]) - $0; + $4 = 1; + } + $0 = $4; + global$0 = $3 + 16 | 0; + return $0; + } + + function FLAC__bitreader_read_raw_uint64($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0; + $3 = global$0 - 16 | 0; + global$0 = $3; + $4 = $1; + $5 = $1; + label$1 : { + label$2 : { + if ($2 >>> 0 >= 33) { + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, $2 + -32 | 0)) { + break label$1 + } + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, 32)) { + break label$1 + } + $0 = HEAP32[$3 + 12 >> 2]; + $2 = 0; + HEAP32[$1 >> 2] = $2; + HEAP32[$1 + 4 >> 2] = $0; + $1 = HEAP32[$3 + 8 >> 2] | $2; + break label$2; + } + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $2)) { + break label$1 + } + $0 = 0; + $1 = HEAP32[$3 + 8 >> 2]; + } + HEAP32[$5 >> 2] = $1; + HEAP32[$4 + 4 >> 2] = $0; + $6 = 1; + } + global$0 = $3 + 16 | 0; + return $6; + } + + function FLAC__bitreader_read_uint32_little_endian($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + HEAP32[$2 + 8 >> 2] = 0; + label$1 : { + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 8 | 0, 8)) { + break label$1 + } + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { + break label$1 + } + $3 = HEAP32[$2 + 8 >> 2] | HEAP32[$2 + 12 >> 2] << 8; + HEAP32[$2 + 8 >> 2] = $3; + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { + break label$1 + } + $3 = $3 | HEAP32[$2 + 12 >> 2] << 16; + HEAP32[$2 + 8 >> 2] = $3; + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { + break label$1 + } + $0 = $3 | HEAP32[$2 + 12 >> 2] << 24; + HEAP32[$2 + 8 >> 2] = $0; + HEAP32[$1 >> 2] = $0; + $4 = 1; + } + global$0 = $2 + 16 | 0; + return $4; + } + + function FLAC__bitreader_skip_bits_no_crc($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0; + $3 = global$0 - 16 | 0; + global$0 = $3; + $4 = 1; + label$1 : { + if (!$1) { + break label$1 + } + $2 = HEAP32[$0 + 20 >> 2] & 7; + label$2 : { + if ($2) { + $2 = 8 - $2 | 0; + $2 = $2 >>> 0 < $1 >>> 0 ? $2 : $1; + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $2)) { + break label$2 + } + $1 = $1 - $2 | 0; + } + $2 = $1 >>> 3 | 0; + if ($2) { + while (1) { + label$7 : { + if (!HEAP32[$0 + 20 >> 2]) { + if ($2 >>> 0 > 3) { + while (1) { + $5 = HEAP32[$0 + 16 >> 2]; + label$11 : { + if ($5 >>> 0 < HEAPU32[$0 + 8 >> 2]) { + HEAP32[$0 + 16 >> 2] = $5 + 1; + $2 = $2 + -4 | 0; + break label$11; + } + if (!bitreader_read_from_client_($0)) { + break label$2 + } + } + if ($2 >>> 0 > 3) { + continue + } + break; + }; + if (!$2) { + break label$7 + } + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, 8)) { + break label$2 + } + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break; + }; + break label$7; + } + if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, 8)) { + break label$2 + } + $2 = $2 + -1 | 0; + if ($2) { + continue + } + } + break; + }; + $1 = $1 & 7; + } + if (!$1) { + break label$1 + } + if (FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $1)) { + break label$1 + } + } + $4 = 0; + } + global$0 = $3 + 16 | 0; + return $4; + } + + function FLAC__bitreader_skip_byte_block_aligned_no_crc($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + $3 = 1; + label$1 : { + if (!$1) { + break label$1 + } + while (1) { + label$3 : { + if (!HEAP32[$0 + 20 >> 2]) { + label$5 : { + if ($1 >>> 0 < 4) { + break label$5 + } + while (1) { + $4 = HEAP32[$0 + 16 >> 2]; + label$7 : { + if ($4 >>> 0 < HEAPU32[$0 + 8 >> 2]) { + HEAP32[$0 + 16 >> 2] = $4 + 1; + $1 = $1 + -4 | 0; + break label$7; + } + if (!bitreader_read_from_client_($0)) { + break label$3 + } + } + if ($1 >>> 0 > 3) { + continue + } + break; + }; + if ($1) { + break label$5 + } + break label$1; + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { + break label$3 + } + $1 = $1 + -1 | 0; + if ($1) { + continue + } + break; + }; + break label$1; + } + if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { + break label$3 + } + $1 = $1 + -1 | 0; + if ($1) { + continue + } + break label$1; + } + break; + }; + $3 = 0; + } + global$0 = $2 + 16 | 0; + return $3; + } + + function FLAC__bitreader_read_byte_block_aligned_no_crc($0, $1, $2) { + var $3 = 0, $4 = 0; + $4 = global$0 - 16 | 0; + global$0 = $4; + label$1 : { + if (!$2) { + $3 = 1; + break label$1; + } + while (1) { + if (!HEAP32[$0 + 20 >> 2]) { + label$5 : { + if ($2 >>> 0 < 4) { + break label$5 + } + while (1) { + label$7 : { + $3 = HEAP32[$0 + 16 >> 2]; + if ($3 >>> 0 < HEAPU32[$0 + 8 >> 2]) { + HEAP32[$0 + 16 >> 2] = $3 + 1; + $3 = HEAP32[HEAP32[$0 >> 2] + ($3 << 2) >> 2]; + $3 = $3 << 24 | $3 << 8 & 16711680 | ($3 >>> 8 & 65280 | $3 >>> 24); + HEAP8[$1 | 0] = $3; + HEAP8[$1 + 1 | 0] = $3 >>> 8; + HEAP8[$1 + 2 | 0] = $3 >>> 16; + HEAP8[$1 + 3 | 0] = $3 >>> 24; + $2 = $2 + -4 | 0; + $1 = $1 + 4 | 0; + break label$7; + } + if (bitreader_read_from_client_($0)) { + break label$7 + } + $3 = 0; + break label$1; + } + if ($2 >>> 0 > 3) { + continue + } + break; + }; + if ($2) { + break label$5 + } + $3 = 1; + break label$1; + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $4 + 12 | 0, 8)) { + $3 = 0; + break label$1; + } + HEAP8[$1 | 0] = HEAP32[$4 + 12 >> 2]; + $3 = 1; + $1 = $1 + 1 | 0; + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break; + }; + break label$1; + } + if (!FLAC__bitreader_read_raw_uint32($0, $4 + 12 | 0, 8)) { + $3 = 0; + break label$1; + } + HEAP8[$1 | 0] = HEAP32[$4 + 12 >> 2]; + $3 = 1; + $1 = $1 + 1 | 0; + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break; + }; + } + global$0 = $4 + 16 | 0; + return $3; + } + + function FLAC__bitreader_read_unary_unsigned($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + HEAP32[$1 >> 2] = 0; + label$1 : { + while (1) { + $3 = HEAP32[$0 + 16 >> 2]; + label$3 : { + if ($3 >>> 0 >= HEAPU32[$0 + 8 >> 2]) { + $2 = HEAP32[$0 + 20 >> 2]; + break label$3; + } + $2 = HEAP32[$0 + 20 >> 2]; + $4 = HEAP32[$0 >> 2]; + while (1) { + $3 = HEAP32[$4 + ($3 << 2) >> 2] << $2; + if ($3) { + $2 = $1; + $4 = HEAP32[$1 >> 2]; + $1 = Math_clz32($3); + HEAP32[$2 >> 2] = $4 + $1; + $2 = ($1 + HEAP32[$0 + 20 >> 2] | 0) + 1 | 0; + HEAP32[$0 + 20 >> 2] = $2; + $1 = 1; + if ($2 >>> 0 < 32) { + break label$1 + } + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; + return 1; + } + HEAP32[$1 >> 2] = (HEAP32[$1 >> 2] - $2 | 0) + 32; + $2 = 0; + HEAP32[$0 + 20 >> 2] = 0; + $3 = HEAP32[$0 + 16 >> 2] + 1 | 0; + HEAP32[$0 + 16 >> 2] = $3; + if ($3 >>> 0 < HEAPU32[$0 + 8 >> 2]) { + continue + } + break; + }; + } + $4 = HEAP32[$0 + 12 >> 2] << 3; + if ($4 >>> 0 > $2 >>> 0) { + $3 = (HEAP32[HEAP32[$0 >> 2] + ($3 << 2) >> 2] & -1 << 32 - $4) << $2; + if ($3) { + $2 = $1; + $4 = HEAP32[$1 >> 2]; + $1 = Math_clz32($3); + HEAP32[$2 >> 2] = $4 + $1; + HEAP32[$0 + 20 >> 2] = ($1 + HEAP32[$0 + 20 >> 2] | 0) + 1; + return 1; + } + HEAP32[$1 >> 2] = HEAP32[$1 >> 2] + ($4 - $2 | 0); + HEAP32[$0 + 20 >> 2] = $4; + } + if (bitreader_read_from_client_($0)) { + continue + } + break; + }; + $1 = 0; + } + return $1; + } + + function FLAC__bitreader_read_rice_signed_block($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; + $6 = global$0 - 16 | 0; + global$0 = $6; + $12 = ($2 << 2) + $1 | 0; + label$1 : { + if (!$3) { + $14 = 1; + if (($2 | 0) < 1) { + break label$1 + } + while (1) { + if (!FLAC__bitreader_read_unary_unsigned($0, $6 + 8 | 0)) { + $14 = 0; + break label$1; + } + $2 = HEAP32[$6 + 8 >> 2]; + HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); + $1 = $1 + 4 | 0; + if ($1 >>> 0 < $12 >>> 0) { + continue + } + break; + }; + break label$1; + } + label$5 : { + label$6 : { + $4 = HEAP32[$0 + 16 >> 2]; + $10 = HEAP32[$0 + 8 >> 2]; + if ($4 >>> 0 >= $10 >>> 0) { + break label$6 + } + $11 = HEAP32[$0 >> 2]; + $13 = HEAP32[$0 + 20 >> 2]; + $9 = HEAP32[$11 + ($4 << 2) >> 2] << $13; + $2 = 0; + break label$5; + } + $2 = 1; + } + while (1) { + label$9 : { + label$10 : { + label$11 : { + label$12 : { + if (!$2) { + $5 = 32 - $13 | 0; + label$14 : { + if ($1 >>> 0 < $12 >>> 0) { + $15 = 32 - $3 | 0; + while (1) { + $2 = $4; + $7 = $5; + label$17 : { + if ($9) { + $7 = Math_clz32($9); + $8 = $7; + break label$17; + } + while (1) { + $2 = $2 + 1 | 0; + if ($2 >>> 0 >= $10 >>> 0) { + break label$14 + } + $9 = HEAP32[($2 << 2) + $11 >> 2]; + $8 = Math_clz32($9); + $7 = $8 + $7 | 0; + if (!$9) { + continue + } + break; + }; + } + $4 = $9 << $8 << 1; + $8 = $4 >>> $15 | 0; + HEAP32[$6 + 8 >> 2] = $7; + $5 = ($7 ^ -1) + $5 & 31; + label$20 : { + if ($5 >>> 0 >= $3 >>> 0) { + $9 = $4 << $3; + $5 = $5 - $3 | 0; + $4 = $2; + break label$20; + } + $4 = $2 + 1 | 0; + if ($4 >>> 0 >= $10 >>> 0) { + break label$12 + } + $2 = HEAP32[($4 << 2) + $11 >> 2]; + $5 = $5 + $15 | 0; + $9 = $2 << 32 - $5; + $8 = $2 >>> $5 | $8; + } + HEAP32[$6 + 12 >> 2] = $8; + $2 = $7 << $3 | $8; + HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); + $1 = $1 + 4 | 0; + if ($1 >>> 0 < $12 >>> 0) { + continue + } + break; + }; + } + $1 = $4 >>> 0 < $10 >>> 0; + HEAP32[$0 + 16 >> 2] = ($1 & !$5) + $4; + HEAP32[$0 + 20 >> 2] = 32 - ($5 ? $5 : $1 << 5); + $14 = 1; + break label$1; + } + HEAP32[$0 + 20 >> 2] = 0; + $2 = $4 + 1 | 0; + HEAP32[$0 + 16 >> 2] = $10 >>> 0 > $2 >>> 0 ? $10 : $2; + break label$10; + } + if (!FLAC__bitreader_read_unary_unsigned($0, $6 + 8 | 0)) { + break label$1 + } + $7 = HEAP32[$6 + 8 >> 2] + $7 | 0; + HEAP32[$6 + 8 >> 2] = $7; + $8 = 0; + $5 = 0; + break label$11; + } + HEAP32[$0 + 16 >> 2] = $4; + HEAP32[$0 + 20 >> 2] = 0; + } + if (!FLAC__bitreader_read_raw_uint32($0, $6 + 12 | 0, $3 - $5 | 0)) { + break label$1 + } + $2 = $7 << $3; + $4 = HEAP32[$6 + 12 >> 2] | $8; + HEAP32[$6 + 12 >> 2] = $4; + $7 = 0; + $2 = $2 | $4; + HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); + $11 = HEAP32[$0 >> 2]; + $4 = HEAP32[$0 + 16 >> 2]; + $13 = HEAP32[$0 + 20 >> 2]; + $9 = HEAP32[$11 + ($4 << 2) >> 2] << $13; + $10 = HEAP32[$0 + 8 >> 2]; + $1 = $1 + 4 | 0; + if ($4 >>> 0 < $10 >>> 0 | $1 >>> 0 >= $12 >>> 0) { + break label$9 + } + } + $2 = 1; + continue; + } + $2 = 0; + continue; + }; + } + global$0 = $6 + 16 | 0; + return $14; + } + + function FLAC__bitreader_read_utf8_uint32($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $7 = global$0 - 16 | 0; + global$0 = $7; + label$1 : { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + break label$1 + } + $4 = HEAP32[$7 + 12 >> 2]; + if ($2) { + $5 = HEAP32[$3 >> 2]; + HEAP32[$3 >> 2] = $5 + 1; + HEAP8[$2 + $5 | 0] = $4; + } + label$3 : { + label$4 : { + label$5 : { + label$6 : { + if (!($4 & 128)) { + break label$6 + } + label$7 : { + if (!(!($4 & 192) | $4 & 32)) { + $6 = 31; + $5 = 1; + break label$7; + } + if (!(!($4 & 224) | $4 & 16)) { + $6 = 15; + $5 = 2; + break label$7; + } + if (!(!($4 & 240) | $4 & 8)) { + $6 = 7; + $5 = 3; + break label$7; + } + if ($4 & 248) { + $6 = 3; + $5 = 4; + if (!($4 & 4)) { + break label$7 + } + } + if (!($4 & 252) | $4 & 2) { + break label$5 + } + $6 = 1; + $5 = 5; + } + $4 = $4 & $6; + if (!$2) { + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + break label$1 + } + $2 = HEAP32[$7 + 12 >> 2]; + if (($2 & 192) != 128) { + break label$4 + } + $4 = $2 & 63 | $4 << 6; + $5 = $5 + -1 | 0; + if ($5) { + continue + } + break label$6; + } + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + break label$1 + } + $6 = HEAP32[$7 + 12 >> 2]; + $8 = HEAP32[$3 >> 2]; + HEAP32[$3 >> 2] = $8 + 1; + HEAP8[$2 + $8 | 0] = $6; + if (($6 & 192) != 128) { + break label$4 + } + $4 = $6 & 63 | $4 << 6; + $5 = $5 + -1 | 0; + if ($5) { + continue + } + break; + }; + } + HEAP32[$1 >> 2] = $4; + break label$3; + } + HEAP32[$1 >> 2] = -1; + break label$3; + } + HEAP32[$1 >> 2] = -1; + } + $9 = 1; + } + global$0 = $7 + 16 | 0; + return $9; + } + + function FLAC__bitreader_read_utf8_uint64($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $7 = global$0 - 16 | 0; + global$0 = $7; + label$1 : { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + break label$1 + } + $4 = HEAP32[$7 + 12 >> 2]; + if ($2) { + $6 = HEAP32[$3 >> 2]; + HEAP32[$3 >> 2] = $6 + 1; + HEAP8[$2 + $6 | 0] = $4; + } + label$4 : { + label$5 : { + label$6 : { + label$7 : { + if ($4 & 128) { + if (!(!($4 & 192) | $4 & 32)) { + $4 = $4 & 31; + $5 = 1; + break label$7; + } + if (!(!($4 & 224) | $4 & 16)) { + $4 = $4 & 15; + $5 = 2; + break label$7; + } + if (!(!($4 & 240) | $4 & 8)) { + $4 = $4 & 7; + $5 = 3; + break label$7; + } + if (!(!($4 & 248) | $4 & 4)) { + $4 = $4 & 3; + $5 = 4; + break label$7; + } + if (!(!($4 & 252) | $4 & 2)) { + $4 = $4 & 1; + $5 = 5; + break label$7; + } + $5 = 1; + if (!(!($4 & 254) | $4 & 1)) { + $5 = 6; + $4 = 0; + break label$7; + } + HEAP32[$1 >> 2] = -1; + HEAP32[$1 + 4 >> 2] = -1; + break label$1; + } + $6 = 0; + break label$6; + } + $6 = 0; + if (!$2) { + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + $5 = 0; + break label$1; + } + $2 = HEAP32[$7 + 12 >> 2]; + if (($2 & 192) != 128) { + break label$5 + } + $2 = $2 & 63; + $6 = $6 << 6 | $4 >>> 26; + $4 = $2 | $4 << 6; + $5 = $5 + -1 | 0; + if ($5) { + continue + } + break label$6; + } + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { + $5 = 0; + break label$1; + } + $8 = HEAP32[$7 + 12 >> 2]; + $9 = HEAP32[$3 >> 2]; + HEAP32[$3 >> 2] = $9 + 1; + HEAP8[$2 + $9 | 0] = $8; + if (($8 & 192) != 128) { + break label$5 + } + $6 = $6 << 6 | $4 >>> 26; + $4 = $8 & 63 | $4 << 6; + $5 = $5 + -1 | 0; + if ($5) { + continue + } + break; + }; + } + HEAP32[$1 >> 2] = $4; + HEAP32[$1 + 4 >> 2] = $6; + break label$4; + } + HEAP32[$1 >> 2] = -1; + HEAP32[$1 + 4 >> 2] = -1; + } + $5 = 1; + } + global$0 = $7 + 16 | 0; + return $5; + } + + function qsort($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0; + $2 = global$0 - 208 | 0; + global$0 = $2; + HEAP32[$2 + 8 >> 2] = 1; + HEAP32[$2 + 12 >> 2] = 0; + label$1 : { + $5 = Math_imul($1, 24); + if (!$5) { + break label$1 + } + HEAP32[$2 + 16 >> 2] = 24; + HEAP32[$2 + 20 >> 2] = 24; + $1 = 24; + $4 = $1; + $3 = 2; + while (1) { + $6 = $4 + 24 | 0; + $4 = $1; + $1 = $1 + $6 | 0; + HEAP32[($2 + 16 | 0) + ($3 << 2) >> 2] = $1; + $3 = $3 + 1 | 0; + if ($1 >>> 0 < $5 >>> 0) { + continue + } + break; + }; + $4 = ($0 + $5 | 0) + -24 | 0; + label$3 : { + if ($4 >>> 0 <= $0 >>> 0) { + $3 = 1; + $1 = 1; + break label$3; + } + $3 = 1; + $1 = 1; + while (1) { + label$6 : { + if (($3 & 3) == 3) { + sift($0, $1, $2 + 16 | 0); + shr($2 + 8 | 0, 2); + $1 = $1 + 2 | 0; + break label$6; + } + $3 = $1 + -1 | 0; + label$8 : { + if (HEAPU32[($2 + 16 | 0) + ($3 << 2) >> 2] >= $4 - $0 >>> 0) { + trinkle($0, $2 + 8 | 0, $1, 0, $2 + 16 | 0); + break label$8; + } + sift($0, $1, $2 + 16 | 0); + } + if (($1 | 0) == 1) { + shl($2 + 8 | 0, 1); + $1 = 0; + break label$6; + } + shl($2 + 8 | 0, $3); + $1 = 1; + } + $3 = HEAP32[$2 + 8 >> 2] | 1; + HEAP32[$2 + 8 >> 2] = $3; + $0 = $0 + 24 | 0; + if ($0 >>> 0 < $4 >>> 0) { + continue + } + break; + }; + } + trinkle($0, $2 + 8 | 0, $1, 0, $2 + 16 | 0); + while (1) { + label$12 : { + label$13 : { + label$14 : { + if (!(($1 | 0) != 1 | ($3 | 0) != 1)) { + if (HEAP32[$2 + 12 >> 2]) { + break label$14 + } + break label$1; + } + if (($1 | 0) > 1) { + break label$13 + } + } + $4 = pntz($2 + 8 | 0); + shr($2 + 8 | 0, $4); + $3 = HEAP32[$2 + 8 >> 2]; + $1 = $1 + $4 | 0; + break label$12; + } + shl($2 + 8 | 0, 2); + HEAP32[$2 + 8 >> 2] = HEAP32[$2 + 8 >> 2] ^ 7; + shr($2 + 8 | 0, 1); + $5 = $0 + -24 | 0; + $4 = $1 + -2 | 0; + trinkle($5 - HEAP32[($2 + 16 | 0) + ($4 << 2) >> 2] | 0, $2 + 8 | 0, $1 + -1 | 0, 1, $2 + 16 | 0); + shl($2 + 8 | 0, 1); + $3 = HEAP32[$2 + 8 >> 2] | 1; + HEAP32[$2 + 8 >> 2] = $3; + trinkle($5, $2 + 8 | 0, $4, 1, $2 + 16 | 0); + $1 = $4; + } + $0 = $0 + -24 | 0; + continue; + }; + } + global$0 = $2 + 208 | 0; + } + + function sift($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $3 = global$0 - 240 | 0; + global$0 = $3; + HEAP32[$3 >> 2] = $0; + $6 = 1; + label$1 : { + if (($1 | 0) < 2) { + break label$1 + } + $4 = $0; + while (1) { + $5 = $4 + -24 | 0; + $7 = $1 + -2 | 0; + $4 = $5 - HEAP32[($7 << 2) + $2 >> 2] | 0; + if ((FUNCTION_TABLE[1]($0, $4) | 0) >= 0) { + if ((FUNCTION_TABLE[1]($0, $5) | 0) > -1) { + break label$1 + } + } + $0 = ($6 << 2) + $3 | 0; + label$4 : { + if ((FUNCTION_TABLE[1]($4, $5) | 0) >= 0) { + HEAP32[$0 >> 2] = $4; + $7 = $1 + -1 | 0; + break label$4; + } + HEAP32[$0 >> 2] = $5; + $4 = $5; + } + $6 = $6 + 1 | 0; + if (($7 | 0) < 2) { + break label$1 + } + $0 = HEAP32[$3 >> 2]; + $1 = $7; + continue; + }; + } + cycle($3, $6); + global$0 = $3 + 240 | 0; + } + + function shr($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $4 = $0; + label$1 : { + if ($1 >>> 0 <= 31) { + $2 = HEAP32[$0 >> 2]; + $3 = HEAP32[$0 + 4 >> 2]; + break label$1; + } + $2 = HEAP32[$0 + 4 >> 2]; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 >> 2] = $2; + $1 = $1 + -32 | 0; + $3 = 0; + } + HEAP32[$4 + 4 >> 2] = $3 >>> $1; + HEAP32[$0 >> 2] = $3 << 32 - $1 | $2 >>> $1; + } + + function trinkle($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0; + $5 = global$0 - 240 | 0; + global$0 = $5; + $6 = HEAP32[$1 >> 2]; + HEAP32[$5 + 232 >> 2] = $6; + $1 = HEAP32[$1 + 4 >> 2]; + HEAP32[$5 >> 2] = $0; + HEAP32[$5 + 236 >> 2] = $1; + $7 = 1; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + if ($1 ? 0 : ($6 | 0) == 1) { + break label$4 + } + $6 = $0 - HEAP32[($2 << 2) + $4 >> 2] | 0; + if ((FUNCTION_TABLE[1]($6, $0) | 0) < 1) { + break label$4 + } + $8 = !$3; + while (1) { + label$6 : { + $1 = $6; + if (!(!$8 | ($2 | 0) < 2)) { + $3 = HEAP32[(($2 << 2) + $4 | 0) + -8 >> 2]; + $6 = $0 + -24 | 0; + if ((FUNCTION_TABLE[1]($6, $1) | 0) > -1) { + break label$6 + } + if ((FUNCTION_TABLE[1]($6 - $3 | 0, $1) | 0) > -1) { + break label$6 + } + } + HEAP32[($7 << 2) + $5 >> 2] = $1; + $0 = pntz($5 + 232 | 0); + shr($5 + 232 | 0, $0); + $7 = $7 + 1 | 0; + $2 = $0 + $2 | 0; + if (HEAP32[$5 + 236 >> 2] ? 0 : HEAP32[$5 + 232 >> 2] == 1) { + break label$2 + } + $3 = 0; + $8 = 1; + $0 = $1; + $6 = $1 - HEAP32[($2 << 2) + $4 >> 2] | 0; + if ((FUNCTION_TABLE[1]($6, HEAP32[$5 >> 2]) | 0) > 0) { + continue + } + break label$3; + } + break; + }; + $1 = $0; + break label$2; + } + $1 = $0; + } + if ($3) { + break label$1 + } + } + cycle($5, $7); + sift($1, $2, $4); + } + global$0 = $5 + 240 | 0; + } + + function shl($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $4 = $0; + label$1 : { + if ($1 >>> 0 <= 31) { + $2 = HEAP32[$0 + 4 >> 2]; + $3 = HEAP32[$0 >> 2]; + break label$1; + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 >> 2] = 0; + $1 = $1 + -32 | 0; + $3 = 0; + } + HEAP32[$4 >> 2] = $3 << $1; + HEAP32[$0 + 4 >> 2] = $2 << $1 | $3 >>> 32 - $1; + } + + function pntz($0) { + var $1 = 0; + $1 = __wasm_ctz_i32(HEAP32[$0 >> 2] + -1 | 0); + if (!$1) { + $0 = __wasm_ctz_i32(HEAP32[$0 + 4 >> 2]); + return $0 ? $0 + 32 | 0 : 0; + } + return $1; + } + + function cycle($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $3 = 24; + $4 = global$0 - 256 | 0; + global$0 = $4; + label$1 : { + if (($1 | 0) < 2) { + break label$1 + } + $7 = ($1 << 2) + $0 | 0; + HEAP32[$7 >> 2] = $4; + $2 = $4; + while (1) { + $5 = $3 >>> 0 < 256 ? $3 : 256; + memcpy($2, HEAP32[$0 >> 2], $5); + $2 = 0; + while (1) { + $6 = ($2 << 2) + $0 | 0; + $2 = $2 + 1 | 0; + memcpy(HEAP32[$6 >> 2], HEAP32[($2 << 2) + $0 >> 2], $5); + HEAP32[$6 >> 2] = HEAP32[$6 >> 2] + $5; + if (($1 | 0) != ($2 | 0)) { + continue + } + break; + }; + $3 = $3 - $5 | 0; + if (!$3) { + break label$1 + } + $2 = HEAP32[$7 >> 2]; + continue; + }; + } + global$0 = $4 + 256 | 0; + } + + function FLAC__format_sample_rate_is_subset($0) { + if ($0 + -1 >>> 0 <= 655349) { + return !(($0 >>> 0) % 10) | (!(($0 >>> 0) % 1e3) | $0 >>> 0 < 65536) + } + return 0; + } + + function FLAC__format_seektable_is_legal($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $3 = HEAP32[$0 >> 2]; + if (!$3) { + return 1 + } + $6 = HEAP32[$0 + 4 >> 2]; + $0 = 0; + $4 = 1; + while (1) { + $7 = $2; + $5 = $1; + $1 = Math_imul($0, 24) + $6 | 0; + $2 = HEAP32[$1 >> 2]; + $1 = HEAP32[$1 + 4 >> 2]; + if (!(($2 | 0) == -1 & ($1 | 0) == -1 | $4 | (($1 | 0) == ($5 | 0) & $2 >>> 0 > $7 >>> 0 | $1 >>> 0 > $5 >>> 0))) { + return 0 + } + $4 = 0; + $0 = $0 + 1 | 0; + if ($0 >>> 0 < $3 >>> 0) { + continue + } + break; + }; + return 1; + } + + function FLAC__format_seektable_sort($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + label$1 : { + $2 = HEAP32[$0 >> 2]; + if (!$2) { + break label$1 + } + qsort(HEAP32[$0 + 4 >> 2], $2); + if (!HEAP32[$0 >> 2]) { + break label$1 + } + $2 = 1; + $1 = HEAP32[$0 >> 2]; + if ($1 >>> 0 > 1) { + $6 = 1; + while (1) { + $4 = HEAP32[$0 + 4 >> 2]; + $3 = $4 + Math_imul($6, 24) | 0; + $5 = HEAP32[$3 >> 2]; + $7 = HEAP32[$3 + 4 >> 2]; + $8 = $7; + label$4 : { + if (($5 | 0) != -1 | ($7 | 0) != -1) { + $7 = $5; + $5 = ($4 + Math_imul($2, 24) | 0) + -24 | 0; + if (($7 | 0) == HEAP32[$5 >> 2] & HEAP32[$5 + 4 >> 2] == ($8 | 0)) { + break label$4 + } + } + $5 = HEAP32[$3 + 4 >> 2]; + $1 = $4 + Math_imul($2, 24) | 0; + HEAP32[$1 >> 2] = HEAP32[$3 >> 2]; + HEAP32[$1 + 4 >> 2] = $5; + $4 = HEAP32[$3 + 20 >> 2]; + HEAP32[$1 + 16 >> 2] = HEAP32[$3 + 16 >> 2]; + HEAP32[$1 + 20 >> 2] = $4; + $4 = HEAP32[$3 + 12 >> 2]; + HEAP32[$1 + 8 >> 2] = HEAP32[$3 + 8 >> 2]; + HEAP32[$1 + 12 >> 2] = $4; + $2 = $2 + 1 | 0; + $1 = HEAP32[$0 >> 2]; + } + $6 = $6 + 1 | 0; + if ($6 >>> 0 < $1 >>> 0) { + continue + } + break; + }; + } + if ($2 >>> 0 >= $1 >>> 0) { + break label$1 + } + $3 = HEAP32[$0 + 4 >> 2]; + while (1) { + $0 = $3 + Math_imul($2, 24) | 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 >> 2] = -1; + HEAP32[$0 + 4 >> 2] = -1; + $2 = $2 + 1 | 0; + if (($1 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + } + + function seekpoint_compare_($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0, $3 = 0; + $2 = HEAP32[$0 + 4 >> 2]; + $3 = HEAP32[$1 + 4 >> 2]; + $0 = HEAP32[$0 >> 2]; + $1 = HEAP32[$1 >> 2]; + return (($0 | 0) == ($1 | 0) & ($2 | 0) == ($3 | 0) ? 0 : ($2 | 0) == ($3 | 0) & $0 >>> 0 < $1 >>> 0 | $2 >>> 0 < $3 >>> 0 ? -1 : 1) | 0; + } + + function utf8len_($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0; + $2 = 1; + label$1 : { + $1 = HEAPU8[$0 | 0]; + label$2 : { + if (!($1 & 128)) { + break label$2 + } + if (!(($1 & 224) != 192 | (HEAPU8[$0 + 1 | 0] & 192) != 128)) { + return (($1 & 254) != 192) << 1 + } + label$4 : { + if (($1 & 240) != 224) { + break label$4 + } + $3 = HEAPU8[$0 + 1 | 0]; + if (($3 & 192) != 128) { + break label$4 + } + $4 = HEAPU8[$0 + 2 | 0]; + if (($4 & 192) != 128) { + break label$4 + } + $2 = 0; + if (($3 & 224) == 128 ? ($1 | 0) == 224 : 0) { + break label$2 + } + label$5 : { + label$6 : { + switch ($1 + -237 | 0) { + case 0: + if (($3 & 224) != 160) { + break label$5 + } + break label$2; + case 2: + break label$6; + default: + break label$5; + }; + } + if (($3 | 0) != 191) { + break label$5 + } + if (($4 & 254) == 190) { + break label$2 + } + } + return 3; + } + label$8 : { + if (($1 & 248) != 240) { + break label$8 + } + $2 = HEAPU8[$0 + 1 | 0]; + if (($2 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128) { + break label$8 + } + if ((HEAPU8[$0 + 3 | 0] & 192) == 128) { + break label$1 + } + } + label$9 : { + if (($1 & 252) != 248) { + break label$9 + } + $2 = HEAPU8[$0 + 1 | 0]; + if (($2 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128 | ((HEAPU8[$0 + 3 | 0] & 192) != 128 | (HEAPU8[$0 + 4 | 0] & 192) != 128)) { + break label$9 + } + return ($1 | 0) == 248 ? (($2 & 248) == 128 ? 0 : 5) : 5; + } + $2 = 0; + if (($1 & 254) != 252) { + break label$2 + } + $3 = HEAPU8[$0 + 1 | 0]; + if (($3 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128 | ((HEAPU8[$0 + 3 | 0] & 192) != 128 | (HEAPU8[$0 + 4 | 0] & 192) != 128)) { + break label$2 + } + if ((HEAPU8[$0 + 5 | 0] & 192) != 128) { + break label$2 + } + $2 = ($1 | 0) == 252 ? (($3 & 252) == 128 ? 0 : 6) : 6; + } + return $2; + } + return ($1 | 0) == 240 ? (($2 & 240) != 128) << 2 : 4; + } + + function FLAC__format_cuesheet_is_legal($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + if ($1) { + $1 = HEAP32[$0 + 140 >> 2]; + $3 = $1; + $2 = HEAP32[$0 + 136 >> 2]; + if (!$1 & $2 >>> 0 <= 88199 | $1 >>> 0 < 0) { + $0 = 0; + break label$1; + } + if (__wasm_i64_urem($2, $3) | i64toi32_i32$HIGH_BITS) { + $0 = 0; + break label$1; + } + $3 = HEAP32[$0 + 148 >> 2]; + if (!$3) { + break label$2 + } + if (HEAPU8[(HEAP32[$0 + 152 >> 2] + ($3 << 5) | 0) + -24 | 0] == 170) { + break label$7 + } + $0 = 0; + break label$1; + } + $2 = HEAP32[$0 + 148 >> 2]; + if (!$2) { + break label$2 + } + $4 = $2 + -1 | 0; + $6 = HEAP32[$0 + 152 >> 2]; + $1 = 0; + while (1) { + $0 = $6 + ($1 << 5) | 0; + if (!HEAPU8[$0 + 8 | 0]) { + break label$3 + } + $3 = HEAPU8[$0 + 23 | 0]; + label$12 : { + label$13 : { + if ($1 >>> 0 < $4 >>> 0) { + if (!$3) { + break label$4 + } + if (HEAPU8[HEAP32[$0 + 24 >> 2] + 8 | 0] > 1) { + break label$5 + } + break label$13; + } + if (!$3) { + break label$12 + } + } + $7 = $0 + 24 | 0; + $0 = 0; + while (1) { + if ($0) { + $5 = HEAP32[$7 >> 2] + ($0 << 4) | 0; + if ((HEAPU8[$5 + -8 | 0] + 1 | 0) != HEAPU8[$5 + 8 | 0]) { + break label$6 + } + } + $0 = $0 + 1 | 0; + if ($0 >>> 0 < $3 >>> 0) { + continue + } + break; + }; + } + $0 = 1; + $1 = $1 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + $6 = $3 + -1 | 0; + $7 = HEAP32[$0 + 152 >> 2]; + $1 = 0; + while (1) { + $0 = $7 + ($1 << 5) | 0; + $2 = HEAPU8[$0 + 8 | 0]; + if (!$2) { + break label$3 + } + if (!(($2 | 0) == 170 | $2 >>> 0 < 100)) { + $0 = 0; + break label$1; + } + if (__wasm_i64_urem(HEAP32[$0 >> 2], HEAP32[$0 + 4 >> 2]) | i64toi32_i32$HIGH_BITS) { + $0 = 0; + break label$1; + } + $2 = HEAPU8[$0 + 23 | 0]; + label$21 : { + label$22 : { + if ($1 >>> 0 < $6 >>> 0) { + if (!$2) { + break label$4 + } + if (HEAPU8[HEAP32[$0 + 24 >> 2] + 8 | 0] < 2) { + break label$22 + } + break label$5; + } + if (!$2) { + break label$21 + } + } + $5 = HEAP32[$0 + 24 >> 2]; + $0 = 0; + while (1) { + $4 = $5 + ($0 << 4) | 0; + if (__wasm_i64_urem(HEAP32[$4 >> 2], HEAP32[$4 + 4 >> 2]) | i64toi32_i32$HIGH_BITS) { + $0 = 0; + break label$1; + } + if (HEAPU8[$4 + 8 | 0] != (HEAPU8[$4 + -8 | 0] + 1 | 0) ? $0 : 0) { + break label$6 + } + $0 = $0 + 1 | 0; + if ($0 >>> 0 < $2 >>> 0) { + continue + } + break; + }; + } + $0 = 1; + $1 = $1 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + $0 = 0; + break label$1; + } + $0 = 0; + break label$1; + } + $0 = 0; + break label$1; + } + $0 = 0; + break label$1; + } + $0 = 0; + } + return $0; + } + + function FLAC__format_picture_is_legal($0) { + var $1 = 0, $2 = 0; + label$1 : { + label$2 : { + $2 = HEAP32[$0 + 4 >> 2]; + $1 = HEAPU8[$2 | 0]; + if (!$1) { + break label$2 + } + while (1) { + if (($1 + -32 & 255) >>> 0 < 95) { + $2 = $2 + 1 | 0; + $1 = HEAPU8[$2 | 0]; + if ($1) { + continue + } + break label$2; + } + break; + }; + $2 = 0; + break label$1; + } + $2 = 1; + $1 = HEAP32[$0 + 8 >> 2]; + if (!HEAPU8[$1 | 0]) { + break label$1 + } + while (1) { + $0 = utf8len_($1); + if (!$0) { + $2 = 0; + break label$1; + } + $1 = $0 + $1 | 0; + if (HEAPU8[$1 | 0]) { + continue + } + break; + }; + } + return $2; + } + + function FLAC__format_get_max_rice_partition_order_from_blocksize_limited_max_and_predictor_order($0, $1, $2) { + var $3 = 0; + while (1) { + $3 = $0; + if ($3) { + $0 = $3 + -1 | 0; + if ($1 >>> $3 >>> 0 <= $2 >>> 0) { + continue + } + } + break; + }; + return $3; + } + + function FLAC__format_get_max_rice_partition_order_from_blocksize($0) { + var $1 = 0, $2 = 0; + label$1 : { + if (!($0 & 1)) { + while (1) { + $1 = $1 + 1 | 0; + $2 = $0 & 2; + $0 = $0 >>> 1 | 0; + if (!$2) { + continue + } + break; + }; + $0 = 15; + if ($1 >>> 0 > 14) { + break label$1 + } + } + $0 = $1; + } + return $0; + } + + function FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0) { + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + } + + function FLAC__format_entropy_coding_method_partitioned_rice_contents_clear($0) { + var $1 = 0; + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 4 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + } + + function FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0; + $3 = 1; + label$1 : { + if (HEAPU32[$0 + 8 >> 2] >= $1 >>> 0) { + break label$1 + } + $3 = HEAP32[$0 >> 2]; + $4 = 4 << $1; + $2 = dlrealloc($3, $4); + if (!($2 | $1 >>> 0 > 29)) { + dlfree($3) + } + HEAP32[$0 >> 2] = $2; + $3 = 0; + if (!$2) { + break label$1 + } + $5 = HEAP32[$0 + 4 >> 2]; + $2 = dlrealloc($5, $4); + if (!($2 | $1 >>> 0 > 29)) { + dlfree($5) + } + HEAP32[$0 + 4 >> 2] = $2; + if (!$2) { + break label$1 + } + memset($2, $4); + HEAP32[$0 + 8 >> 2] = $1; + $3 = 1; + } + return $3; + } + + function ogg_page_serialno($0) { + $0 = HEAP32[$0 >> 2]; + return HEAPU8[$0 + 14 | 0] | HEAPU8[$0 + 15 | 0] << 8 | (HEAPU8[$0 + 16 | 0] << 16 | HEAPU8[$0 + 17 | 0] << 24); + } + + function ogg_stream_init($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + if ($0) { + memset($0 + 8 | 0, 352); + HEAP32[$0 + 24 >> 2] = 1024; + HEAP32[$0 + 4 >> 2] = 16384; + $3 = dlmalloc(16384); + HEAP32[$0 >> 2] = $3; + $2 = dlmalloc(4096); + HEAP32[$0 + 16 >> 2] = $2; + $4 = dlmalloc(8192); + HEAP32[$0 + 20 >> 2] = $4; + label$2 : { + if ($3) { + if ($2 ? $4 : 0) { + break label$2 + } + dlfree($3); + $2 = HEAP32[$0 + 16 >> 2]; + } + if ($2) { + dlfree($2) + } + $1 = HEAP32[$0 + 20 >> 2]; + if ($1) { + dlfree($1) + } + memset($0, 360); + return -1; + } + HEAP32[$0 + 336 >> 2] = $1; + $0 = 0; + } else { + $0 = -1 + } + return $0; + } + + function ogg_stream_clear($0) { + var $1 = 0; + if ($0) { + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 16 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 20 >> 2]; + if ($1) { + dlfree($1) + } + memset($0, 360); + } + } + + function ogg_page_checksum_set($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0; + if ($0) { + HEAP8[HEAP32[$0 >> 2] + 22 | 0] = 0; + HEAP8[HEAP32[$0 >> 2] + 23 | 0] = 0; + HEAP8[HEAP32[$0 >> 2] + 24 | 0] = 0; + HEAP8[HEAP32[$0 >> 2] + 25 | 0] = 0; + $3 = HEAP32[$0 + 4 >> 2]; + if (($3 | 0) >= 1) { + $4 = HEAP32[$0 >> 2]; + while (1) { + $1 = HEAP32[((HEAPU8[$2 + $4 | 0] ^ $1 >>> 24) << 2) + 6512 >> 2] ^ $1 << 8; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + $3 = HEAP32[$0 + 12 >> 2]; + if (($3 | 0) >= 1) { + $4 = HEAP32[$0 + 8 >> 2]; + $2 = 0; + while (1) { + $1 = HEAP32[((HEAPU8[$2 + $4 | 0] ^ $1 >>> 24) << 2) + 6512 >> 2] ^ $1 << 8; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + HEAP8[HEAP32[$0 >> 2] + 22 | 0] = $1; + HEAP8[HEAP32[$0 >> 2] + 23 | 0] = $1 >>> 8; + HEAP8[HEAP32[$0 >> 2] + 24 | 0] = $1 >>> 16; + HEAP8[HEAP32[$0 >> 2] + 25 | 0] = $1 >>> 24; + } + } + + function ogg_stream_iovecin($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; + $6 = -1; + folding_inner0 : { + label$1 : { + if (!$0) { + break label$1 + } + $8 = HEAP32[$0 >> 2]; + if (!$8) { + break label$1 + } + if (!$1) { + return 0 + } + while (1) { + $7 = HEAP32[(($5 << 3) + $1 | 0) + 4 >> 2]; + if (($7 | 0) < 0 | ($9 | 0) > (2147483647 - $7 | 0)) { + break label$1 + } + $9 = $7 + $9 | 0; + $5 = $5 + 1 | 0; + if (($5 | 0) != 1) { + continue + } + break; + }; + $5 = HEAP32[$0 + 12 >> 2]; + if ($5) { + $7 = HEAP32[$0 + 8 >> 2] - $5 | 0; + HEAP32[$0 + 8 >> 2] = $7; + if ($7) { + memmove($8, $5 + $8 | 0, $7) + } + HEAP32[$0 + 12 >> 2] = 0; + } + $5 = HEAP32[$0 + 4 >> 2]; + if (($5 - $9 | 0) <= HEAP32[$0 + 8 >> 2]) { + if (($5 | 0) > (2147483647 - $9 | 0)) { + break folding_inner0 + } + $5 = $5 + $9 | 0; + $5 = ($5 | 0) < 2147482623 ? $5 + 1024 | 0 : $5; + $8 = dlrealloc(HEAP32[$0 >> 2], $5); + if (!$8) { + break folding_inner0 + } + HEAP32[$0 >> 2] = $8; + HEAP32[$0 + 4 >> 2] = $5; + } + $8 = ($9 | 0) / 255 | 0; + $11 = $8 + 1 | 0; + if (_os_lacing_expand($0, $11)) { + break label$1 + } + $6 = HEAP32[$0 + 8 >> 2]; + $5 = 0; + while (1) { + $7 = HEAP32[$0 >> 2] + $6 | 0; + $6 = ($5 << 3) + $1 | 0; + memcpy($7, HEAP32[$6 >> 2], HEAP32[$6 + 4 >> 2]); + $6 = HEAP32[$0 + 8 >> 2] + HEAP32[$6 + 4 >> 2] | 0; + HEAP32[$0 + 8 >> 2] = $6; + $5 = $5 + 1 | 0; + if (($5 | 0) != 1) { + continue + } + break; + }; + $7 = HEAP32[$0 + 16 >> 2]; + $12 = $7; + $1 = HEAP32[$0 + 28 >> 2]; + $13 = $1; + label$19 : { + if (($9 | 0) <= 254) { + $6 = HEAP32[$0 + 20 >> 2]; + $5 = 0; + break label$19; + } + $6 = HEAP32[$0 + 20 >> 2]; + $5 = 0; + while (1) { + $10 = $1 + $5 | 0; + HEAP32[$7 + ($10 << 2) >> 2] = 255; + $14 = HEAP32[$0 + 356 >> 2]; + $10 = ($10 << 3) + $6 | 0; + HEAP32[$10 >> 2] = HEAP32[$0 + 352 >> 2]; + HEAP32[$10 + 4 >> 2] = $14; + $5 = $5 + 1 | 0; + if (($8 | 0) != ($5 | 0)) { + continue + } + break; + }; + $5 = $8; + } + $5 = $13 + $5 | 0; + HEAP32[$12 + ($5 << 2) >> 2] = $9 - Math_imul($8, 255); + $5 = ($5 << 3) + $6 | 0; + HEAP32[$5 >> 2] = $3; + HEAP32[$5 + 4 >> 2] = $4; + HEAP32[$0 + 352 >> 2] = $3; + HEAP32[$0 + 356 >> 2] = $4; + $3 = $7 + ($1 << 2) | 0; + HEAP32[$3 >> 2] = HEAP32[$3 >> 2] | 256; + HEAP32[$0 + 28 >> 2] = $1 + $11; + $1 = HEAP32[$0 + 348 >> 2]; + $3 = HEAP32[$0 + 344 >> 2] + 1 | 0; + if ($3 >>> 0 < 1) { + $1 = $1 + 1 | 0 + } + HEAP32[$0 + 344 >> 2] = $3; + HEAP32[$0 + 348 >> 2] = $1; + $6 = 0; + if (!$2) { + break label$1 + } + HEAP32[$0 + 328 >> 2] = 1; + } + return $6; + } + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 16 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 20 >> 2]; + if ($1) { + dlfree($1) + } + memset($0, 360); + return -1; + } + + function _os_lacing_expand($0, $1) { + var $2 = 0; + folding_inner0 : { + $2 = HEAP32[$0 + 24 >> 2]; + if (($2 - $1 | 0) <= HEAP32[$0 + 28 >> 2]) { + if (($2 | 0) > (2147483647 - $1 | 0)) { + break folding_inner0 + } + $1 = $1 + $2 | 0; + $1 = ($1 | 0) < 2147483615 ? $1 + 32 | 0 : $1; + $2 = dlrealloc(HEAP32[$0 + 16 >> 2], $1 << 2); + if (!$2) { + break folding_inner0 + } + HEAP32[$0 + 16 >> 2] = $2; + $2 = dlrealloc(HEAP32[$0 + 20 >> 2], $1 << 3); + if (!$2) { + break folding_inner0 + } + HEAP32[$0 + 24 >> 2] = $1; + HEAP32[$0 + 20 >> 2] = $2; + } + return 0; + } + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 16 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 20 >> 2]; + if ($1) { + dlfree($1) + } + memset($0, 360); + return -1; + } + + function ogg_stream_packetin($0, $1) { + var $2 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + HEAP32[$2 + 8 >> 2] = HEAP32[$1 >> 2]; + HEAP32[$2 + 12 >> 2] = HEAP32[$1 + 4 >> 2]; + $0 = ogg_stream_iovecin($0, $2 + 8 | 0, HEAP32[$1 + 12 >> 2], HEAP32[$1 + 16 >> 2], HEAP32[$1 + 20 >> 2]); + global$0 = $2 + 16 | 0; + return $0; + } + + function ogg_stream_flush_i($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; + label$1 : { + if (!$0) { + break label$1 + } + $8 = HEAP32[$0 + 28 >> 2]; + $4 = ($8 | 0) < 255 ? $8 : 255; + if (!$4) { + break label$1 + } + $10 = HEAP32[$0 >> 2]; + if (!$10) { + break label$1 + } + label$2 : { + label$3 : { + label$4 : { + $11 = HEAP32[$0 + 332 >> 2]; + if ($11) { + if (($8 | 0) >= 1) { + break label$4 + } + $7 = -1; + $5 = -1; + break label$3; + } + $3 = ($4 | 0) > 0 ? $4 : 0; + while (1) { + if (($3 | 0) == ($6 | 0)) { + break label$3 + } + $9 = $6 << 2; + $4 = $6 + 1 | 0; + $6 = $4; + if (HEAPU8[$9 + HEAP32[$0 + 16 >> 2] | 0] == 255) { + continue + } + break; + }; + $3 = $4; + break label$3; + } + $4 = ($4 | 0) > 1 ? $4 : 1; + $7 = -1; + $5 = -1; + label$7 : { + while (1) { + if (!(($6 | 0) <= 4096 | ($9 | 0) <= 3)) { + $2 = 1; + break label$7; + } + $9 = 0; + $12 = HEAPU8[HEAP32[$0 + 16 >> 2] + ($3 << 2) | 0]; + if (($12 | 0) != 255) { + $13 = $13 + 1 | 0; + $9 = $13; + $5 = HEAP32[$0 + 20 >> 2] + ($3 << 3) | 0; + $7 = HEAP32[$5 >> 2]; + $5 = HEAP32[$5 + 4 >> 2]; + } + $6 = $6 + $12 | 0; + $3 = $3 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + $3 = $4; + } + $4 = 255; + if (($3 | 0) == 255) { + break label$2 + } + } + $4 = $3; + if (!$2) { + break label$1 + } + } + HEAP32[$0 + 40 >> 2] = 1399285583; + HEAP16[$0 + 44 >> 1] = 0; + $2 = HEAP32[$0 + 16 >> 2]; + $3 = (HEAPU8[$2 + 1 | 0] ^ -1) & 1; + $3 = $11 ? $3 : $3 | 2; + HEAP8[$0 + 45 | 0] = $3; + if (!(!HEAP32[$0 + 328 >> 2] | ($4 | 0) != ($8 | 0))) { + HEAP8[$0 + 45 | 0] = $3 | 4 + } + HEAP32[$0 + 332 >> 2] = 1; + HEAP8[$0 + 53 | 0] = $5 >>> 24; + HEAP8[$0 + 52 | 0] = $5 >>> 16; + HEAP8[$0 + 51 | 0] = $5 >>> 8; + HEAP8[$0 + 50 | 0] = $5; + HEAP8[$0 + 49 | 0] = ($5 & 16777215) << 8 | $7 >>> 24; + HEAP8[$0 + 48 | 0] = ($5 & 65535) << 16 | $7 >>> 16; + HEAP8[$0 + 47 | 0] = ($5 & 255) << 24 | $7 >>> 8; + HEAP8[$0 + 46 | 0] = $7; + $3 = HEAP32[$0 + 336 >> 2]; + HEAP8[$0 + 54 | 0] = $3; + HEAP8[$0 + 55 | 0] = $3 >>> 8; + HEAP8[$0 + 56 | 0] = $3 >>> 16; + HEAP8[$0 + 57 | 0] = $3 >>> 24; + $3 = HEAP32[$0 + 340 >> 2]; + if (($3 | 0) == -1) { + HEAP32[$0 + 340 >> 2] = 0; + $3 = 0; + } + HEAP8[$0 + 66 | 0] = $4; + $6 = 0; + HEAP16[$0 + 62 >> 1] = 0; + HEAP16[$0 + 64 >> 1] = 0; + HEAP8[$0 + 61 | 0] = $3 >>> 24; + HEAP8[$0 + 60 | 0] = $3 >>> 16; + HEAP8[$0 + 59 | 0] = $3 >>> 8; + HEAP8[$0 + 58 | 0] = $3; + $14 = 1; + HEAP32[$0 + 340 >> 2] = $3 + 1; + if (($4 | 0) >= 1) { + $3 = 0; + while (1) { + $5 = HEAP32[$2 + ($3 << 2) >> 2]; + HEAP8[($0 + $3 | 0) + 67 | 0] = $5; + $6 = ($5 & 255) + $6 | 0; + $3 = $3 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + HEAP32[$1 >> 2] = $0 + 40; + $3 = $4 + 27 | 0; + HEAP32[$0 + 324 >> 2] = $3; + HEAP32[$1 + 4 >> 2] = $3; + $3 = HEAP32[$0 + 12 >> 2]; + HEAP32[$1 + 12 >> 2] = $6; + HEAP32[$1 + 8 >> 2] = $3 + $10; + $3 = $8 - $4 | 0; + HEAP32[$0 + 28 >> 2] = $3; + memmove($2, $2 + ($4 << 2) | 0, $3 << 2); + $2 = HEAP32[$0 + 20 >> 2]; + memmove($2, $2 + ($4 << 3) | 0, HEAP32[$0 + 28 >> 2] << 3); + HEAP32[$0 + 12 >> 2] = HEAP32[$0 + 12 >> 2] + $6; + if (!$1) { + break label$1 + } + $0 = 0; + HEAP8[HEAP32[$1 >> 2] + 22 | 0] = 0; + HEAP8[HEAP32[$1 >> 2] + 23 | 0] = 0; + HEAP8[HEAP32[$1 >> 2] + 24 | 0] = 0; + HEAP8[HEAP32[$1 >> 2] + 25 | 0] = 0; + $2 = HEAP32[$1 + 4 >> 2]; + if (($2 | 0) >= 1) { + $4 = HEAP32[$1 >> 2]; + $3 = 0; + while (1) { + $0 = HEAP32[((HEAPU8[$3 + $4 | 0] ^ $0 >>> 24) << 2) + 6512 >> 2] ^ $0 << 8; + $3 = $3 + 1 | 0; + if (($2 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + $2 = HEAP32[$1 + 12 >> 2]; + if (($2 | 0) >= 1) { + $4 = HEAP32[$1 + 8 >> 2]; + $3 = 0; + while (1) { + $0 = HEAP32[((HEAPU8[$3 + $4 | 0] ^ $0 >>> 24) << 2) + 6512 >> 2] ^ $0 << 8; + $3 = $3 + 1 | 0; + if (($2 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + HEAP8[HEAP32[$1 >> 2] + 22 | 0] = $0; + HEAP8[HEAP32[$1 >> 2] + 23 | 0] = $0 >>> 8; + HEAP8[HEAP32[$1 >> 2] + 24 | 0] = $0 >>> 16; + HEAP8[HEAP32[$1 >> 2] + 25 | 0] = $0 >>> 24; + } + return $14; + } + + function ogg_stream_pageout($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + if (!(!$0 | !HEAP32[$0 >> 2])) { + $2 = HEAP32[$0 + 28 >> 2]; + $4 = $0; + label$2 : { + label$3 : { + if (HEAP32[$0 + 328 >> 2]) { + if ($2) { + break label$3 + } + $3 = 0; + break label$2; + } + $3 = 0; + if (HEAP32[$0 + 332 >> 2] | !$2) { + break label$2 + } + } + $3 = 1; + } + $2 = ogg_stream_flush_i($4, $1, $3); + } + return $2; + } + + function ogg_sync_init($0) { + if ($0) { + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + } + return 0; + } + + function ogg_sync_clear($0) { + var $1 = 0; + if ($0) { + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + } + } + + function ogg_sync_buffer($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $2 = HEAP32[$0 + 4 >> 2]; + if (($2 | 0) >= 0) { + $4 = HEAP32[$0 + 12 >> 2]; + if ($4) { + $3 = HEAP32[$0 + 8 >> 2] - $4 | 0; + HEAP32[$0 + 8 >> 2] = $3; + if (($3 | 0) >= 1) { + $2 = HEAP32[$0 >> 2]; + memmove($2, $2 + $4 | 0, $3); + $2 = HEAP32[$0 + 4 >> 2]; + } + HEAP32[$0 + 12 >> 2] = 0; + } + $3 = $2; + $2 = HEAP32[$0 + 8 >> 2]; + label$4 : { + if (($3 - $2 | 0) >= ($1 | 0)) { + $1 = HEAP32[$0 >> 2]; + break label$4; + } + $2 = ($1 + $2 | 0) + 4096 | 0; + $1 = HEAP32[$0 >> 2]; + label$6 : { + if ($1) { + $1 = dlrealloc($1, $2); + break label$6; + } + $1 = dlmalloc($2); + } + if (!$1) { + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + return 0; + } + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 >> 2] = $1; + $2 = HEAP32[$0 + 8 >> 2]; + } + $0 = $1 + $2 | 0; + } else { + $0 = 0 + } + return $0; + } + + function ogg_sync_wrote($0, $1) { + var $2 = 0, $3 = 0; + $2 = -1; + $3 = HEAP32[$0 + 4 >> 2]; + label$1 : { + if (($3 | 0) < 0) { + break label$1 + } + $1 = HEAP32[$0 + 8 >> 2] + $1 | 0; + if (($1 | 0) > ($3 | 0)) { + break label$1 + } + HEAP32[$0 + 8 >> 2] = $1; + $2 = 0; + } + return $2; + } + + function ogg_sync_pageseek($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0; + $9 = global$0 - 16 | 0; + global$0 = $9; + label$1 : { + if (HEAP32[$0 + 4 >> 2] < 0) { + break label$1 + } + $4 = HEAP32[$0 + 12 >> 2]; + $10 = HEAP32[$0 + 8 >> 2] - $4 | 0; + $2 = $4 + HEAP32[$0 >> 2] | 0; + label$2 : { + label$3 : { + label$4 : { + $5 = HEAP32[$0 + 20 >> 2]; + label$5 : { + if (!$5) { + if (($10 | 0) < 27) { + break label$1 + } + if ((HEAPU8[$2 | 0] | HEAPU8[$2 + 1 | 0] << 8 | (HEAPU8[$2 + 2 | 0] << 16 | HEAPU8[$2 + 3 | 0] << 24)) != 1399285583) { + break label$5 + } + $4 = HEAPU8[$2 + 26 | 0]; + $5 = $4 + 27 | 0; + if (($10 | 0) < ($5 | 0)) { + break label$1 + } + if ($4) { + $4 = HEAP32[$0 + 24 >> 2]; + while (1) { + $4 = HEAPU8[($2 + $6 | 0) + 27 | 0] + $4 | 0; + HEAP32[$0 + 24 >> 2] = $4; + $6 = $6 + 1 | 0; + if ($6 >>> 0 < HEAPU8[$2 + 26 | 0]) { + continue + } + break; + }; + } + HEAP32[$0 + 20 >> 2] = $5; + } + if ((HEAP32[$0 + 24 >> 2] + $5 | 0) > ($10 | 0)) { + break label$1 + } + $7 = HEAPU8[$2 + 22 | 0] | HEAPU8[$2 + 23 | 0] << 8 | (HEAPU8[$2 + 24 | 0] << 16 | HEAPU8[$2 + 25 | 0] << 24); + HEAP32[$9 + 12 >> 2] = $7; + $6 = 0; + HEAP8[$2 + 22 | 0] = 0; + HEAP8[$2 + 23 | 0] = 0; + HEAP8[$2 + 24 | 0] = 0; + HEAP8[$2 + 25 | 0] = 0; + $11 = HEAP32[$0 + 24 >> 2]; + $8 = HEAP32[$0 + 20 >> 2]; + HEAP8[$2 + 22 | 0] = 0; + HEAP8[$2 + 23 | 0] = 0; + HEAP8[$2 + 24 | 0] = 0; + HEAP8[$2 + 25 | 0] = 0; + if (($8 | 0) > 0) { + $5 = 0; + while (1) { + $3 = HEAP32[((HEAPU8[$2 + $5 | 0] ^ $3 >>> 24) << 2) + 6512 >> 2] ^ $3 << 8; + $5 = $5 + 1 | 0; + if (($8 | 0) != ($5 | 0)) { + continue + } + break; + }; + } + $4 = $2 + 22 | 0; + if (($11 | 0) > 0) { + $8 = $2 + $8 | 0; + while (1) { + $3 = HEAP32[((HEAPU8[$6 + $8 | 0] ^ $3 >>> 24) << 2) + 6512 >> 2] ^ $3 << 8; + $6 = $6 + 1 | 0; + if (($11 | 0) != ($6 | 0)) { + continue + } + break; + }; + } + HEAP8[$2 + 22 | 0] = $3; + HEAP8[$2 + 23 | 0] = $3 >>> 8; + HEAP8[$2 + 24 | 0] = $3 >>> 16; + HEAP8[$2 + 25 | 0] = $3 >>> 24; + if (HEAP32[$9 + 12 >> 2] == (HEAPU8[$4 | 0] | HEAPU8[$4 + 1 | 0] << 8 | (HEAPU8[$4 + 2 | 0] << 16 | HEAPU8[$4 + 3 | 0] << 24))) { + break label$4 + } + HEAP8[$4 | 0] = $7; + HEAP8[$4 + 1 | 0] = $7 >>> 8; + HEAP8[$4 + 2 | 0] = $7 >>> 16; + HEAP8[$4 + 3 | 0] = $7 >>> 24; + } + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + $3 = memchr($2 + 1 | 0, $10 + -1 | 0); + if (!$3) { + break label$3 + } + $6 = HEAP32[$0 >> 2]; + break label$2; + } + $7 = HEAP32[$0 + 12 >> 2]; + label$13 : { + if (!$1) { + $5 = HEAP32[$0 + 24 >> 2]; + $3 = HEAP32[$0 + 20 >> 2]; + break label$13; + } + $4 = $7 + HEAP32[$0 >> 2] | 0; + HEAP32[$1 >> 2] = $4; + $3 = HEAP32[$0 + 20 >> 2]; + HEAP32[$1 + 4 >> 2] = $3; + HEAP32[$1 + 8 >> 2] = $3 + $4; + $5 = HEAP32[$0 + 24 >> 2]; + HEAP32[$1 + 12 >> 2] = $5; + } + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + $3 = $3 + $5 | 0; + HEAP32[$0 + 12 >> 2] = $7 + $3; + break label$1; + } + $6 = HEAP32[$0 >> 2]; + $3 = $6 + HEAP32[$0 + 8 >> 2] | 0; + } + HEAP32[$0 + 12 >> 2] = $3 - $6; + $3 = $2 - $3 | 0; + } + global$0 = $9 + 16 | 0; + return $3; + } + + function ogg_sync_pageout($0, $1) { + var $2 = 0; + if (HEAP32[$0 + 4 >> 2] >= 0) { + while (1) { + $2 = ogg_sync_pageseek($0, $1); + if (($2 | 0) > 0) { + return 1 + } + if (!$2) { + return 0 + } + if (HEAP32[$0 + 16 >> 2]) { + continue + } + break; + }; + HEAP32[$0 + 16 >> 2] = 1; + $0 = -1; + } else { + $0 = 0 + } + return $0; + } + + function ogg_stream_pagein($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; + $4 = -1; + folding_inner0 : { + label$1 : { + if (!$0) { + break label$1 + } + $6 = HEAP32[$0 >> 2]; + if (!$6) { + break label$1 + } + $3 = HEAP32[$1 >> 2]; + $10 = HEAPU8[$3 + 5 | 0]; + $5 = HEAP32[$1 + 12 >> 2]; + $11 = HEAP32[$1 + 8 >> 2]; + $8 = HEAPU8[$3 + 26 | 0]; + $13 = HEAPU8[$3 + 18 | 0] | HEAPU8[$3 + 19 | 0] << 8 | (HEAPU8[$3 + 20 | 0] << 16 | HEAPU8[$3 + 21 | 0] << 24); + $9 = HEAPU8[$3 + 14 | 0] | HEAPU8[$3 + 15 | 0] << 8 | (HEAPU8[$3 + 16 | 0] << 16 | HEAPU8[$3 + 17 | 0] << 24); + $14 = HEAPU8[$3 + 6 | 0] | HEAPU8[$3 + 7 | 0] << 8 | (HEAPU8[$3 + 8 | 0] << 16 | HEAPU8[$3 + 9 | 0] << 24); + $15 = HEAPU8[$3 + 10 | 0] | HEAPU8[$3 + 11 | 0] << 8 | (HEAPU8[$3 + 12 | 0] << 16 | HEAPU8[$3 + 13 | 0] << 24); + $12 = HEAPU8[$3 + 4 | 0]; + $2 = HEAP32[$0 + 36 >> 2]; + $1 = HEAP32[$0 + 12 >> 2]; + if ($1) { + $7 = HEAP32[$0 + 8 >> 2] - $1 | 0; + HEAP32[$0 + 8 >> 2] = $7; + if ($7) { + memmove($6, $1 + $6 | 0, $7) + } + HEAP32[$0 + 12 >> 2] = 0; + } + if ($2) { + $1 = $0; + $6 = HEAP32[$0 + 28 >> 2] - $2 | 0; + if ($6) { + $7 = HEAP32[$0 + 16 >> 2]; + memmove($7, $7 + ($2 << 2) | 0, $6 << 2); + $6 = HEAP32[$0 + 20 >> 2]; + memmove($6, $6 + ($2 << 3) | 0, HEAP32[$0 + 28 >> 2] - $2 << 3); + $7 = HEAP32[$0 + 28 >> 2] - $2 | 0; + } else { + $7 = 0 + } + HEAP32[$1 + 28 >> 2] = $7; + HEAP32[$0 + 36 >> 2] = 0; + HEAP32[$0 + 32 >> 2] = HEAP32[$0 + 32 >> 2] - $2; + } + if (($9 | 0) != HEAP32[$0 + 336 >> 2] | $12) { + break label$1 + } + if (_os_lacing_expand($0, $8 + 1 | 0)) { + break label$1 + } + $7 = $10 & 1; + $6 = HEAP32[$0 + 340 >> 2]; + label$7 : { + if (($6 | 0) == ($13 | 0)) { + break label$7 + } + $2 = HEAP32[$0 + 32 >> 2]; + $9 = HEAP32[$0 + 28 >> 2]; + if (($2 | 0) < ($9 | 0)) { + $4 = HEAP32[$0 + 8 >> 2]; + $12 = HEAP32[$0 + 16 >> 2]; + $1 = $2; + while (1) { + $4 = $4 - HEAPU8[$12 + ($1 << 2) | 0] | 0; + $1 = $1 + 1 | 0; + if (($1 | 0) < ($9 | 0)) { + continue + } + break; + }; + HEAP32[$0 + 8 >> 2] = $4; + } + HEAP32[$0 + 28 >> 2] = $2; + if (($6 | 0) == -1) { + break label$7 + } + $1 = $2 + 1 | 0; + HEAP32[$0 + 28 >> 2] = $1; + HEAP32[HEAP32[$0 + 16 >> 2] + ($2 << 2) >> 2] = 1024; + HEAP32[$0 + 32 >> 2] = $1; + } + $6 = $10 & 2; + $4 = 0; + label$10 : { + if (!$7) { + break label$10 + } + $1 = HEAP32[$0 + 28 >> 2]; + if (HEAP32[(HEAP32[$0 + 16 >> 2] + ($1 << 2) | 0) + -4 >> 2] != 1024 ? ($1 | 0) >= 1 : 0) { + break label$10 + } + $6 = 0; + if (!$8) { + break label$10 + } + $1 = 0; + while (1) { + $4 = $1 + 1 | 0; + $1 = HEAPU8[($1 + $3 | 0) + 27 | 0]; + $5 = $5 - $1 | 0; + $11 = $1 + $11 | 0; + if (($1 | 0) != 255) { + break label$10 + } + $1 = $4; + if (($8 | 0) != ($1 | 0)) { + continue + } + break; + }; + $4 = $8; + } + if ($5) { + $2 = HEAP32[$0 + 4 >> 2]; + $1 = HEAP32[$0 + 8 >> 2]; + label$15 : { + if (($2 - $5 | 0) > ($1 | 0)) { + $2 = HEAP32[$0 >> 2]; + break label$15; + } + if (($2 | 0) > (2147483647 - $5 | 0)) { + break folding_inner0 + } + $1 = $2 + $5 | 0; + $1 = ($1 | 0) < 2147482623 ? $1 + 1024 | 0 : $1; + $2 = dlrealloc(HEAP32[$0 >> 2], $1); + if (!$2) { + break folding_inner0 + } + HEAP32[$0 >> 2] = $2; + HEAP32[$0 + 4 >> 2] = $1; + $1 = HEAP32[$0 + 8 >> 2]; + } + memcpy($1 + $2 | 0, $11, $5); + HEAP32[$0 + 8 >> 2] = HEAP32[$0 + 8 >> 2] + $5; + } + $11 = $10 & 4; + label$25 : { + if (($4 | 0) >= ($8 | 0)) { + break label$25 + } + $10 = HEAP32[$0 + 20 >> 2]; + $7 = HEAP32[$0 + 16 >> 2]; + $2 = HEAP32[$0 + 28 >> 2]; + $1 = $7 + ($2 << 2) | 0; + $5 = HEAPU8[($3 + $4 | 0) + 27 | 0]; + HEAP32[$1 >> 2] = $5; + $9 = $10 + ($2 << 3) | 0; + HEAP32[$9 >> 2] = -1; + HEAP32[$9 + 4 >> 2] = -1; + if ($6) { + HEAP32[$1 >> 2] = $5 | 256 + } + $1 = $2 + 1 | 0; + HEAP32[$0 + 28 >> 2] = $1; + $4 = $4 + 1 | 0; + label$27 : { + if (($5 | 0) == 255) { + $2 = -1; + break label$27; + } + HEAP32[$0 + 32 >> 2] = $1; + } + if (($4 | 0) != ($8 | 0)) { + while (1) { + $6 = HEAPU8[($3 + $4 | 0) + 27 | 0]; + HEAP32[$7 + ($1 << 2) >> 2] = $6; + $5 = $10 + ($1 << 3) | 0; + HEAP32[$5 >> 2] = -1; + HEAP32[$5 + 4 >> 2] = -1; + $5 = $1 + 1 | 0; + HEAP32[$0 + 28 >> 2] = $5; + $4 = $4 + 1 | 0; + if (($6 | 0) != 255) { + HEAP32[$0 + 32 >> 2] = $5; + $2 = $1; + } + $1 = $5; + if (($4 | 0) != ($8 | 0)) { + continue + } + break; + } + } + if (($2 | 0) == -1) { + break label$25 + } + $1 = HEAP32[$0 + 20 >> 2] + ($2 << 3) | 0; + HEAP32[$1 >> 2] = $14; + HEAP32[$1 + 4 >> 2] = $15; + } + label$32 : { + if (!$11) { + break label$32 + } + HEAP32[$0 + 328 >> 2] = 1; + $1 = HEAP32[$0 + 28 >> 2]; + if (($1 | 0) < 1) { + break label$32 + } + $1 = (HEAP32[$0 + 16 >> 2] + ($1 << 2) | 0) + -4 | 0; + HEAP32[$1 >> 2] = HEAP32[$1 >> 2] | 512; + } + HEAP32[$0 + 340 >> 2] = $13 + 1; + $4 = 0; + } + return $4; + } + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 16 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 20 >> 2]; + if ($1) { + dlfree($1) + } + memset($0, 360); + return -1; + } + + function ogg_sync_reset($0) { + if (HEAP32[$0 + 4 >> 2] < 0) { + return + } + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + } + + function ogg_stream_reset($0) { + if (!$0 | !HEAP32[$0 >> 2]) { + $0 = -1 + } else { + HEAP32[$0 + 344 >> 2] = 0; + HEAP32[$0 + 348 >> 2] = 0; + HEAP32[$0 + 340 >> 2] = -1; + HEAP32[$0 + 332 >> 2] = 0; + HEAP32[$0 + 324 >> 2] = 0; + HEAP32[$0 + 328 >> 2] = 0; + HEAP32[$0 + 36 >> 2] = 0; + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 32 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 + 352 >> 2] = 0; + HEAP32[$0 + 356 >> 2] = 0; + $0 = 0; + } + } + + function ogg_stream_packetout($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + label$1 : { + if (!$0) { + break label$1 + } + $8 = HEAP32[$0 >> 2]; + if (!$8) { + break label$1 + } + $5 = HEAP32[$0 + 36 >> 2]; + if (HEAP32[$0 + 32 >> 2] <= ($5 | 0)) { + break label$1 + } + $3 = HEAP32[$0 + 16 >> 2]; + $6 = HEAP32[$3 + ($5 << 2) >> 2]; + if ($6 & 1024) { + HEAP32[$0 + 36 >> 2] = $5 + 1; + $1 = $0; + $3 = $0; + $2 = HEAP32[$0 + 348 >> 2]; + $0 = HEAP32[$0 + 344 >> 2] + 1 | 0; + if ($0 >>> 0 < 1) { + $2 = $2 + 1 | 0 + } + HEAP32[$3 + 344 >> 2] = $0; + HEAP32[$1 + 348 >> 2] = $2; + return -1; + } + $4 = $6 & 512; + $7 = 255; + $2 = $6 & 255; + label$3 : { + if (($2 | 0) != 255) { + $7 = $2; + break label$3; + } + while (1) { + $5 = $5 + 1 | 0; + $2 = HEAP32[($5 << 2) + $3 >> 2]; + $4 = $2 & 512 ? 512 : $4; + $2 = $2 & 255; + $7 = $2 + $7 | 0; + if (($2 | 0) == 255) { + continue + } + break; + }; + } + label$6 : { + if (!$1) { + $4 = HEAP32[$0 + 344 >> 2]; + $2 = HEAP32[$0 + 348 >> 2]; + $6 = HEAP32[$0 + 12 >> 2]; + break label$6; + } + HEAP32[$1 + 8 >> 2] = $6 & 256; + HEAP32[$1 + 12 >> 2] = $4; + $6 = HEAP32[$0 + 12 >> 2]; + HEAP32[$1 >> 2] = $8 + $6; + $3 = HEAP32[$0 + 348 >> 2]; + $2 = $3; + $4 = HEAP32[$0 + 344 >> 2]; + HEAP32[$1 + 24 >> 2] = $4; + HEAP32[$1 + 28 >> 2] = $2; + $3 = HEAP32[$0 + 20 >> 2] + ($5 << 3) | 0; + $8 = HEAP32[$3 + 4 >> 2]; + $3 = HEAP32[$3 >> 2]; + HEAP32[$1 + 4 >> 2] = $7; + HEAP32[$1 + 16 >> 2] = $3; + HEAP32[$1 + 20 >> 2] = $8; + } + $3 = $4 + 1 | 0; + if ($3 >>> 0 < 1) { + $2 = $2 + 1 | 0 + } + HEAP32[$0 + 344 >> 2] = $3; + HEAP32[$0 + 348 >> 2] = $2; + $4 = 1; + HEAP32[$0 + 36 >> 2] = $5 + 1; + HEAP32[$0 + 12 >> 2] = $6 + $7; + } + return $4; + } + + function FLAC__ogg_decoder_aspect_init($0) { + var $1 = 0; + label$1 : { + if (ogg_stream_init($0 + 8 | 0, HEAP32[$0 + 4 >> 2])) { + break label$1 + } + if (ogg_sync_init($0 + 368 | 0)) { + break label$1 + } + HEAP32[$0 + 396 >> 2] = -1; + HEAP32[$0 + 400 >> 2] = -1; + HEAP32[$0 + 408 >> 2] = 0; + HEAP32[$0 + 412 >> 2] = 0; + HEAP32[$0 + 404 >> 2] = HEAP32[$0 >> 2]; + $1 = 1; + } + return $1; + } + + function FLAC__ogg_decoder_aspect_set_defaults($0) { + HEAP32[$0 >> 2] = 1; + } + + function FLAC__ogg_decoder_aspect_reset($0) { + ogg_stream_reset($0 + 8 | 0); + ogg_sync_reset($0 + 368 | 0); + HEAP32[$0 + 408 >> 2] = 0; + HEAP32[$0 + 412 >> 2] = 0; + if (HEAP32[$0 >> 2]) { + HEAP32[$0 + 404 >> 2] = 1 + } + } + + function FLAC__ogg_decoder_aspect_read_callback_wrapper($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; + $8 = global$0 - 16 | 0; + global$0 = $8; + $9 = HEAP32[$2 >> 2]; + HEAP32[$2 >> 2] = 0; + label$1 : { + label$2 : { + label$3 : { + if (!$9) { + break label$3 + } + $10 = $0 + 416 | 0; + $11 = $0 + 368 | 0; + $13 = $0 + 440 | 0; + $14 = $0 + 8 | 0; + $15 = HEAP32[2721]; + $16 = HEAPU8[7536]; + while (1) { + if (HEAP32[$0 + 408 >> 2]) { + break label$3 + } + label$5 : { + label$6 : { + if (HEAP32[$0 + 412 >> 2]) { + if (HEAP32[$0 + 432 >> 2]) { + $7 = HEAP32[$0 + 440 >> 2]; + $6 = HEAP32[$0 + 444 >> 2]; + $5 = $9 - $5 | 0; + if ($6 >>> 0 > $5 >>> 0) { + break label$6 + } + $1 = memcpy($1, $7, $6); + HEAP32[$2 >> 2] = $6 + HEAP32[$2 >> 2]; + HEAP32[$0 + 432 >> 2] = 0; + $1 = $1 + $6 | 0; + break label$5; + } + $5 = ogg_stream_packetout($14, $13); + if (($5 | 0) >= 1) { + HEAP32[$0 + 432 >> 2] = 1; + $12 = HEAP32[$0 + 444 >> 2]; + if (($12 | 0) < 1) { + break label$5 + } + $6 = HEAP32[$13 >> 2]; + if (HEAPU8[$6 | 0] != ($16 | 0)) { + break label$5 + } + $7 = 3; + if (($12 | 0) < 9) { + break label$1 + } + $5 = $15; + if ((HEAPU8[$6 + 1 | 0] | HEAPU8[$6 + 2 | 0] << 8 | (HEAPU8[$6 + 3 | 0] << 16 | HEAPU8[$6 + 4 | 0] << 24)) != (HEAPU8[$5 | 0] | HEAPU8[$5 + 1 | 0] << 8 | (HEAPU8[$5 + 2 | 0] << 16 | HEAPU8[$5 + 3 | 0] << 24))) { + break label$1 + } + $5 = HEAPU8[$6 + 5 | 0]; + HEAP32[$0 + 396 >> 2] = $5; + HEAP32[$0 + 400 >> 2] = HEAPU8[$6 + 6 | 0]; + if (($5 | 0) != 1) { + $7 = 4; + break label$1; + } + HEAP32[$0 + 444 >> 2] = $12 + -9; + HEAP32[$0 + 440 >> 2] = $6 + 9; + break label$5; + } + if ($5) { + $7 = 2; + break label$1; + } + HEAP32[$0 + 412 >> 2] = 0; + break label$5; + } + $5 = ogg_sync_pageout($11, $10); + if (($5 | 0) >= 1) { + if (HEAP32[$0 + 404 >> 2]) { + $5 = ogg_page_serialno($10); + HEAP32[$0 + 404 >> 2] = 0; + HEAP32[$0 + 344 >> 2] = $5; + HEAP32[$0 + 4 >> 2] = $5; + } + if (ogg_stream_pagein($14, $10)) { + break label$5 + } + HEAP32[$0 + 432 >> 2] = 0; + HEAP32[$0 + 412 >> 2] = 1; + break label$5; + } + if ($5) { + $7 = 2; + break label$1; + } + $5 = $9 - HEAP32[$2 >> 2] | 0; + $5 = $5 >>> 0 > 8192 ? $5 : 8192; + $6 = ogg_sync_buffer($11, $5); + if (!$6) { + $7 = 7; + break label$1; + } + HEAP32[$8 + 12 >> 2] = $5; + label$16 : { + switch ((FUNCTION_TABLE[8]($3, $6, $8 + 12 | 0, $4) | 0) + -1 | 0) { + case 0: + HEAP32[$0 + 408 >> 2] = 1; + break; + case 4: + break label$2; + default: + break label$16; + }; + } + if ((ogg_sync_wrote($11, HEAP32[$8 + 12 >> 2]) | 0) >= 0) { + break label$5 + } + $7 = 6; + break label$1; + } + $1 = memcpy($1, $7, $5); + HEAP32[$2 >> 2] = $5 + HEAP32[$2 >> 2]; + HEAP32[$0 + 440 >> 2] = $5 + HEAP32[$0 + 440 >> 2]; + HEAP32[$0 + 444 >> 2] = HEAP32[$0 + 444 >> 2] - $5; + $1 = $1 + $5 | 0; + } + $5 = HEAP32[$2 >> 2]; + if ($9 >>> 0 > $5 >>> 0) { + continue + } + break; + }; + } + global$0 = $8 + 16 | 0; + return !$5 & HEAP32[$0 + 408 >> 2] != 0; + } + $7 = 5; + } + global$0 = $8 + 16 | 0; + return $7; + } + + function FLAC__MD5Init($0) { + HEAP32[$0 + 80 >> 2] = 0; + HEAP32[$0 + 84 >> 2] = 0; + HEAP32[$0 + 64 >> 2] = 1732584193; + HEAP32[$0 + 68 >> 2] = -271733879; + HEAP32[$0 + 72 >> 2] = -1732584194; + HEAP32[$0 + 76 >> 2] = 271733878; + HEAP32[$0 + 88 >> 2] = 0; + HEAP32[$0 + 92 >> 2] = 0; + } + + function FLAC__MD5Final($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $3 = HEAP32[$1 + 80 >> 2] & 63; + $2 = $3 + $1 | 0; + HEAP8[$2 | 0] = 128; + $2 = $2 + 1 | 0; + $4 = 56; + label$1 : { + if ($3 >>> 0 < 56) { + $4 = 55 - $3 | 0; + break label$1; + } + memset($2, $3 ^ 63); + FLAC__MD5Transform($1 - -64 | 0, $1); + $2 = $1; + } + memset($2, $4); + $2 = HEAP32[$1 + 80 >> 2]; + HEAP32[$1 + 56 >> 2] = $2 << 3; + HEAP32[$1 + 60 >> 2] = HEAP32[$1 + 84 >> 2] << 3 | $2 >>> 29; + FLAC__MD5Transform($1 - -64 | 0, $1); + $2 = HEAPU8[$1 + 76 | 0] | HEAPU8[$1 + 77 | 0] << 8 | (HEAPU8[$1 + 78 | 0] << 16 | HEAPU8[$1 + 79 | 0] << 24); + $3 = HEAPU8[$1 + 72 | 0] | HEAPU8[$1 + 73 | 0] << 8 | (HEAPU8[$1 + 74 | 0] << 16 | HEAPU8[$1 + 75 | 0] << 24); + HEAP8[$0 + 8 | 0] = $3; + HEAP8[$0 + 9 | 0] = $3 >>> 8; + HEAP8[$0 + 10 | 0] = $3 >>> 16; + HEAP8[$0 + 11 | 0] = $3 >>> 24; + HEAP8[$0 + 12 | 0] = $2; + HEAP8[$0 + 13 | 0] = $2 >>> 8; + HEAP8[$0 + 14 | 0] = $2 >>> 16; + HEAP8[$0 + 15 | 0] = $2 >>> 24; + $2 = HEAPU8[$1 + 68 | 0] | HEAPU8[$1 + 69 | 0] << 8 | (HEAPU8[$1 + 70 | 0] << 16 | HEAPU8[$1 + 71 | 0] << 24); + $3 = HEAPU8[$1 + 64 | 0] | HEAPU8[$1 + 65 | 0] << 8 | (HEAPU8[$1 + 66 | 0] << 16 | HEAPU8[$1 + 67 | 0] << 24); + HEAP8[$0 | 0] = $3; + HEAP8[$0 + 1 | 0] = $3 >>> 8; + HEAP8[$0 + 2 | 0] = $3 >>> 16; + HEAP8[$0 + 3 | 0] = $3 >>> 24; + HEAP8[$0 + 4 | 0] = $2; + HEAP8[$0 + 5 | 0] = $2 >>> 8; + HEAP8[$0 + 6 | 0] = $2 >>> 16; + HEAP8[$0 + 7 | 0] = $2 >>> 24; + $0 = HEAP32[$1 + 88 >> 2]; + if ($0) { + dlfree($0); + HEAP32[$1 + 88 >> 2] = 0; + HEAP32[$1 + 92 >> 2] = 0; + } + memset($1, 96); + } + + function FLAC__MD5Transform($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $11 = HEAP32[$1 + 16 >> 2]; + $8 = HEAP32[$1 + 32 >> 2]; + $12 = HEAP32[$1 + 48 >> 2]; + $13 = HEAP32[$1 + 36 >> 2]; + $14 = HEAP32[$1 + 52 >> 2]; + $15 = HEAP32[$1 + 4 >> 2]; + $6 = HEAP32[$1 + 20 >> 2]; + $7 = HEAP32[$0 + 4 >> 2]; + $9 = HEAP32[$1 >> 2]; + $25 = HEAP32[$0 >> 2]; + $16 = HEAP32[$0 + 12 >> 2]; + $10 = HEAP32[$0 + 8 >> 2]; + $3 = $7 + __wasm_rotl_i32((($9 + $25 | 0) + ($16 ^ ($16 ^ $10) & $7) | 0) + -680876936 | 0, 7) | 0; + $17 = HEAP32[$1 + 12 >> 2]; + $18 = HEAP32[$1 + 8 >> 2]; + $4 = __wasm_rotl_i32((($15 + $16 | 0) + ($3 & ($7 ^ $10) ^ $10) | 0) + -389564586 | 0, 12) + $3 | 0; + $2 = __wasm_rotl_i32((($18 + $10 | 0) + ($4 & ($3 ^ $7) ^ $7) | 0) + 606105819 | 0, 17) + $4 | 0; + $5 = __wasm_rotl_i32((($7 + $17 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -1044525330 | 0, 22) + $2 | 0; + $3 = __wasm_rotl_i32((($3 + $11 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + -176418897 | 0, 7) + $5 | 0; + $19 = HEAP32[$1 + 28 >> 2]; + $20 = HEAP32[$1 + 24 >> 2]; + $4 = __wasm_rotl_i32((($4 + $6 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + 1200080426 | 0, 12) + $3 | 0; + $2 = __wasm_rotl_i32((($2 + $20 | 0) + ($5 ^ $4 & ($3 ^ $5)) | 0) + -1473231341 | 0, 17) + $4 | 0; + $5 = __wasm_rotl_i32((($5 + $19 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -45705983 | 0, 22) + $2 | 0; + $3 = __wasm_rotl_i32((($3 + $8 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + 1770035416 | 0, 7) + $5 | 0; + $21 = HEAP32[$1 + 44 >> 2]; + $22 = HEAP32[$1 + 40 >> 2]; + $4 = __wasm_rotl_i32((($4 + $13 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + -1958414417 | 0, 12) + $3 | 0; + $2 = __wasm_rotl_i32((($2 + $22 | 0) + ($5 ^ $4 & ($3 ^ $5)) | 0) + -42063 | 0, 17) + $4 | 0; + $5 = __wasm_rotl_i32((($5 + $21 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -1990404162 | 0, 22) + $2 | 0; + $3 = __wasm_rotl_i32((($3 + $12 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + 1804603682 | 0, 7) + $5 | 0; + $23 = HEAP32[$1 + 56 >> 2]; + $24 = HEAP32[$1 + 60 >> 2]; + $4 = __wasm_rotl_i32((($4 + $14 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + -40341101 | 0, 12) + $3 | 0; + $1 = $4 + __wasm_rotl_i32((($2 + $23 | 0) + ($5 ^ ($3 ^ $5) & $4) | 0) + -1502002290 | 0, 17) | 0; + $26 = $1 + $21 | 0; + $2 = $3 + $15 | 0; + $3 = __wasm_rotl_i32((($5 + $24 | 0) + ($3 ^ $1 & ($3 ^ $4)) | 0) + 1236535329 | 0, 22) + $1 | 0; + $2 = __wasm_rotl_i32(($2 + ($1 ^ ($3 ^ $1) & $4) | 0) + -165796510 | 0, 5) + $3 | 0; + $1 = __wasm_rotl_i32((($4 + $20 | 0) + ($3 ^ $1 & ($3 ^ $2)) | 0) + -1069501632 | 0, 9) + $2 | 0; + $4 = __wasm_rotl_i32(($26 + (($2 ^ $1) & $3 ^ $2) | 0) + 643717713 | 0, 14) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $9 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + -373897302 | 0, 20) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $6 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + -701558691 | 0, 5) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $22 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + 38016083 | 0, 9) + $2 | 0; + $4 = __wasm_rotl_i32((($24 + $4 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + -660478335 | 0, 14) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $11 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + -405537848 | 0, 20) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $13 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + 568446438 | 0, 5) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $23 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + -1019803690 | 0, 9) + $2 | 0; + $4 = __wasm_rotl_i32((($4 + $17 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + -187363961 | 0, 14) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $8 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + 1163531501 | 0, 20) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + -1444681467 | 0, 5) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $18 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + -51403784 | 0, 9) + $2 | 0; + $4 = __wasm_rotl_i32((($4 + $19 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + 1735328473 | 0, 14) + $1 | 0; + $5 = $1 ^ $4; + $3 = __wasm_rotl_i32((($3 + $12 | 0) + ($1 ^ $5 & $2) | 0) + -1926607734 | 0, 20) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $6 | 0) + ($3 ^ $5) | 0) + -378558 | 0, 4) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $8 | 0) + ($3 ^ $4 ^ $2) | 0) + -2022574463 | 0, 11) + $2 | 0; + $4 = __wasm_rotl_i32((($4 + $21 | 0) + ($1 ^ ($3 ^ $2)) | 0) + 1839030562 | 0, 16) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $23 | 0) + ($4 ^ ($1 ^ $2)) | 0) + -35309556 | 0, 23) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $15 | 0) + ($3 ^ ($1 ^ $4)) | 0) + -1530992060 | 0, 4) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $11 | 0) + ($2 ^ ($3 ^ $4)) | 0) + 1272893353 | 0, 11) + $2 | 0; + $4 = __wasm_rotl_i32((($4 + $19 | 0) + ($1 ^ ($3 ^ $2)) | 0) + -155497632 | 0, 16) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $22 | 0) + ($4 ^ ($1 ^ $2)) | 0) + -1094730640 | 0, 23) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($3 ^ ($1 ^ $4)) | 0) + 681279174 | 0, 4) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $9 | 0) + ($2 ^ ($3 ^ $4)) | 0) + -358537222 | 0, 11) + $2 | 0; + $4 = __wasm_rotl_i32((($4 + $17 | 0) + ($1 ^ ($3 ^ $2)) | 0) + -722521979 | 0, 16) + $1 | 0; + $3 = __wasm_rotl_i32((($3 + $20 | 0) + ($4 ^ ($1 ^ $2)) | 0) + 76029189 | 0, 23) + $4 | 0; + $2 = __wasm_rotl_i32((($2 + $13 | 0) + ($3 ^ ($1 ^ $4)) | 0) + -640364487 | 0, 4) + $3 | 0; + $1 = __wasm_rotl_i32((($1 + $12 | 0) + ($2 ^ ($3 ^ $4)) | 0) + -421815835 | 0, 11) + $2 | 0; + $5 = $2 + $9 | 0; + $9 = $1 ^ $2; + $2 = __wasm_rotl_i32((($4 + $24 | 0) + ($1 ^ ($3 ^ $2)) | 0) + 530742520 | 0, 16) + $1 | 0; + $4 = __wasm_rotl_i32((($3 + $18 | 0) + ($9 ^ $2) | 0) + -995338651 | 0, 23) + $2 | 0; + $3 = __wasm_rotl_i32(($5 + (($4 | $1 ^ -1) ^ $2) | 0) + -198630844 | 0, 6) + $4 | 0; + $5 = $4 + $6 | 0; + $6 = $2 + $23 | 0; + $2 = __wasm_rotl_i32((($1 + $19 | 0) + ($4 ^ ($3 | $2 ^ -1)) | 0) + 1126891415 | 0, 10) + $3 | 0; + $4 = __wasm_rotl_i32(($6 + ($3 ^ ($2 | $4 ^ -1)) | 0) + -1416354905 | 0, 15) + $2 | 0; + $1 = __wasm_rotl_i32(($5 + (($4 | $3 ^ -1) ^ $2) | 0) + -57434055 | 0, 21) + $4 | 0; + $5 = $4 + $22 | 0; + $6 = $2 + $17 | 0; + $2 = __wasm_rotl_i32((($3 + $12 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + 1700485571 | 0, 6) + $1 | 0; + $4 = __wasm_rotl_i32(($6 + ($1 ^ ($2 | $4 ^ -1)) | 0) + -1894986606 | 0, 10) + $2 | 0; + $3 = __wasm_rotl_i32(($5 + (($4 | $1 ^ -1) ^ $2) | 0) + -1051523 | 0, 15) + $4 | 0; + $5 = $4 + $24 | 0; + $8 = $2 + $8 | 0; + $2 = __wasm_rotl_i32((($1 + $15 | 0) + ($4 ^ ($3 | $2 ^ -1)) | 0) + -2054922799 | 0, 21) + $3 | 0; + $4 = __wasm_rotl_i32(($8 + ($3 ^ ($2 | $4 ^ -1)) | 0) + 1873313359 | 0, 6) + $2 | 0; + $1 = __wasm_rotl_i32(($5 + (($4 | $3 ^ -1) ^ $2) | 0) + -30611744 | 0, 10) + $4 | 0; + $3 = __wasm_rotl_i32((($3 + $20 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + -1560198380 | 0, 15) + $1 | 0; + $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($1 ^ ($3 | $4 ^ -1)) | 0) + 1309151649 | 0, 21) + $3 | 0; + $4 = __wasm_rotl_i32((($4 + $11 | 0) + (($2 | $1 ^ -1) ^ $3) | 0) + -145523070 | 0, 6) + $2 | 0; + HEAP32[$0 >> 2] = $4 + $25; + $1 = __wasm_rotl_i32((($1 + $21 | 0) + ($2 ^ ($4 | $3 ^ -1)) | 0) + -1120210379 | 0, 10) + $4 | 0; + HEAP32[$0 + 12 >> 2] = $1 + $16; + $3 = __wasm_rotl_i32((($3 + $18 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + 718787259 | 0, 15) + $1 | 0; + HEAP32[$0 + 8 >> 2] = $3 + $10; + (wasm2js_i32$0 = $0, wasm2js_i32$1 = __wasm_rotl_i32((($2 + $13 | 0) + ($1 ^ ($3 | $4 ^ -1)) | 0) + -343485551 | 0, 21) + ($3 + $7 | 0) | 0), HEAP32[wasm2js_i32$0 + 4 >> 2] = wasm2js_i32$1; + } + + function FLAC__MD5Accumulate($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; + __wasm_i64_mul($4, 0, $2, 0); + label$1 : { + if (i64toi32_i32$HIGH_BITS) { + break label$1 + } + $7 = Math_imul($2, $4); + __wasm_i64_mul($3, 0, $7, 0); + if (i64toi32_i32$HIGH_BITS) { + break label$1 + } + $6 = HEAP32[$0 + 88 >> 2]; + $11 = Math_imul($3, $7); + label$2 : { + if (HEAPU32[$0 + 92 >> 2] >= $11 >>> 0) { + $5 = $6; + break label$2; + } + $5 = dlrealloc($6, $11); + label$4 : { + if (!$5) { + dlfree($6); + $5 = dlmalloc($11); + HEAP32[$0 + 88 >> 2] = $5; + if ($5) { + break label$4 + } + HEAP32[$0 + 92 >> 2] = 0; + return 0; + } + HEAP32[$0 + 88 >> 2] = $5; + } + HEAP32[$0 + 92 >> 2] = $11; + } + label$6 : { + label$7 : { + label$8 : { + label$9 : { + label$10 : { + label$11 : { + label$12 : { + label$13 : { + label$14 : { + label$15 : { + label$16 : { + label$17 : { + $6 = Math_imul($4, 100) + $2 | 0; + if (($6 | 0) <= 300) { + label$19 : { + switch ($6 + -101 | 0) { + case 3: + break label$10; + case 5: + break label$11; + case 7: + break label$12; + case 2: + case 4: + case 6: + break label$7; + case 0: + break label$8; + case 1: + break label$9; + default: + break label$19; + }; + } + switch ($6 + -201 | 0) { + case 0: + break label$13; + case 1: + break label$14; + case 3: + break label$15; + case 5: + break label$16; + case 7: + break label$17; + default: + break label$7; + }; + } + label$20 : { + label$21 : { + label$22 : { + switch ($6 + -401 | 0) { + default: + switch ($6 + -301 | 0) { + case 0: + break label$20; + case 1: + break label$21; + default: + break label$7; + }; + case 7: + if (!$3) { + break label$6 + } + $13 = HEAP32[$1 + 28 >> 2]; + $8 = HEAP32[$1 + 24 >> 2]; + $12 = HEAP32[$1 + 20 >> 2]; + $7 = HEAP32[$1 + 16 >> 2]; + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $9 = $2 << 2; + HEAP32[$5 >> 2] = HEAP32[$9 + $1 >> 2]; + HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $9 >> 2]; + HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $9 >> 2]; + HEAP32[$5 + 12 >> 2] = HEAP32[$10 + $9 >> 2]; + HEAP32[$5 + 16 >> 2] = HEAP32[$7 + $9 >> 2]; + HEAP32[$5 + 20 >> 2] = HEAP32[$9 + $12 >> 2]; + HEAP32[$5 + 24 >> 2] = HEAP32[$8 + $9 >> 2]; + HEAP32[$5 + 28 >> 2] = HEAP32[$9 + $13 >> 2]; + $5 = $5 + 32 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + case 5: + if (!$3) { + break label$6 + } + $12 = HEAP32[$1 + 20 >> 2]; + $7 = HEAP32[$1 + 16 >> 2]; + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $8 = $2 << 2; + HEAP32[$5 >> 2] = HEAP32[$8 + $1 >> 2]; + HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $8 >> 2]; + HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $8 >> 2]; + HEAP32[$5 + 12 >> 2] = HEAP32[$8 + $10 >> 2]; + HEAP32[$5 + 16 >> 2] = HEAP32[$7 + $8 >> 2]; + HEAP32[$5 + 20 >> 2] = HEAP32[$8 + $12 >> 2]; + $5 = $5 + 24 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + case 3: + if (!$3) { + break label$6 + } + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $7 = $2 << 2; + HEAP32[$5 >> 2] = HEAP32[$7 + $1 >> 2]; + HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $7 >> 2]; + HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $7 >> 2]; + HEAP32[$5 + 12 >> 2] = HEAP32[$7 + $10 >> 2]; + $5 = $5 + 16 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + case 1: + if (!$3) { + break label$6 + } + $6 = HEAP32[$1 + 4 >> 2]; + $4 = HEAP32[$1 >> 2]; + $1 = 0; + while (1) { + $2 = $1 << 2; + HEAP32[$5 >> 2] = HEAP32[$2 + $4 >> 2]; + HEAP32[$5 + 4 >> 2] = HEAP32[$2 + $6 >> 2]; + $5 = $5 + 8 | 0; + $1 = $1 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$6; + case 0: + break label$22; + case 2: + case 4: + case 6: + break label$7; + }; + } + if (!$3) { + break label$6 + } + $2 = HEAP32[$1 >> 2]; + $1 = 0; + while (1) { + HEAP32[$5 >> 2] = HEAP32[$2 + ($1 << 2) >> 2]; + $5 = $5 + 4 | 0; + $1 = $1 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $2 = 0; + while (1) { + $4 = $2 << 2; + $6 = HEAP32[$4 + HEAP32[$1 >> 2] >> 2]; + HEAP8[$5 | 0] = $6; + HEAP8[$5 + 2 | 0] = $6 >>> 16; + HEAP8[$5 + 1 | 0] = $6 >>> 8; + $4 = HEAP32[$4 + HEAP32[$1 + 4 >> 2] >> 2]; + HEAP8[$5 + 3 | 0] = $4; + HEAP8[$5 + 5 | 0] = $4 >>> 16; + HEAP8[$5 + 4 | 0] = $4 >>> 8; + $5 = $5 + 6 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $2 = 0; + while (1) { + $4 = HEAP32[HEAP32[$1 >> 2] + ($2 << 2) >> 2]; + HEAP8[$5 | 0] = $4; + HEAP8[$5 + 2 | 0] = $4 >>> 16; + HEAP8[$5 + 1 | 0] = $4 >>> 8; + $5 = $5 + 3 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $13 = HEAP32[$1 + 28 >> 2]; + $8 = HEAP32[$1 + 24 >> 2]; + $12 = HEAP32[$1 + 20 >> 2]; + $7 = HEAP32[$1 + 16 >> 2]; + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $9 = $2 << 2; + HEAP16[$5 >> 1] = HEAP32[$9 + $1 >> 2]; + HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $9 >> 2]; + HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $9 >> 2]; + HEAP16[$5 + 6 >> 1] = HEAP32[$10 + $9 >> 2]; + HEAP16[$5 + 8 >> 1] = HEAP32[$7 + $9 >> 2]; + HEAP16[$5 + 10 >> 1] = HEAP32[$9 + $12 >> 2]; + HEAP16[$5 + 12 >> 1] = HEAP32[$8 + $9 >> 2]; + HEAP16[$5 + 14 >> 1] = HEAP32[$9 + $13 >> 2]; + $5 = $5 + 16 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $12 = HEAP32[$1 + 20 >> 2]; + $7 = HEAP32[$1 + 16 >> 2]; + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $8 = $2 << 2; + HEAP16[$5 >> 1] = HEAP32[$8 + $1 >> 2]; + HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $8 >> 2]; + HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $8 >> 2]; + HEAP16[$5 + 6 >> 1] = HEAP32[$8 + $10 >> 2]; + HEAP16[$5 + 8 >> 1] = HEAP32[$7 + $8 >> 2]; + HEAP16[$5 + 10 >> 1] = HEAP32[$8 + $12 >> 2]; + $5 = $5 + 12 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $10 = HEAP32[$1 + 12 >> 2]; + $6 = HEAP32[$1 + 8 >> 2]; + $4 = HEAP32[$1 + 4 >> 2]; + $1 = HEAP32[$1 >> 2]; + $2 = 0; + while (1) { + $7 = $2 << 2; + HEAP16[$5 >> 1] = HEAP32[$7 + $1 >> 2]; + HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $7 >> 2]; + HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $7 >> 2]; + HEAP16[$5 + 6 >> 1] = HEAP32[$7 + $10 >> 2]; + $5 = $5 + 8 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $6 = HEAP32[$1 + 4 >> 2]; + $4 = HEAP32[$1 >> 2]; + $1 = 0; + while (1) { + $2 = $1 << 2; + HEAP16[$5 >> 1] = HEAP32[$2 + $4 >> 2]; + HEAP16[$5 + 2 >> 1] = HEAP32[$2 + $6 >> 2]; + $5 = $5 + 4 | 0; + $1 = $1 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $2 = HEAP32[$1 >> 2]; + $1 = 0; + while (1) { + HEAP16[$5 >> 1] = HEAP32[$2 + ($1 << 2) >> 2]; + $5 = $5 + 2 | 0; + $1 = $1 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $4 = 0; + while (1) { + $2 = $4 << 2; + HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; + HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; + HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; + HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; + HEAP8[$5 + 4 | 0] = HEAP32[$2 + HEAP32[$1 + 16 >> 2] >> 2]; + HEAP8[$5 + 5 | 0] = HEAP32[$2 + HEAP32[$1 + 20 >> 2] >> 2]; + HEAP8[$5 + 6 | 0] = HEAP32[$2 + HEAP32[$1 + 24 >> 2] >> 2]; + HEAP8[$5 + 7 | 0] = HEAP32[$2 + HEAP32[$1 + 28 >> 2] >> 2]; + $5 = $5 + 8 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $4 = 0; + while (1) { + $2 = $4 << 2; + HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; + HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; + HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; + HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; + HEAP8[$5 + 4 | 0] = HEAP32[$2 + HEAP32[$1 + 16 >> 2] >> 2]; + HEAP8[$5 + 5 | 0] = HEAP32[$2 + HEAP32[$1 + 20 >> 2] >> 2]; + $5 = $5 + 6 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $4 = 0; + while (1) { + $2 = $4 << 2; + HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; + HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; + HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; + HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; + $5 = $5 + 4 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $2 = 0; + while (1) { + $4 = $2 << 2; + HEAP8[$5 | 0] = HEAP32[$4 + HEAP32[$1 >> 2] >> 2]; + HEAP8[$5 + 1 | 0] = HEAP32[$4 + HEAP32[$1 + 4 >> 2] >> 2]; + $5 = $5 + 2 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + if (!$3) { + break label$6 + } + $2 = 0; + while (1) { + HEAP8[$5 | 0] = HEAP32[HEAP32[$1 >> 2] + ($2 << 2) >> 2]; + $5 = $5 + 1 | 0; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + break label$6; + } + label$45 : { + switch ($4 + -1 | 0) { + case 3: + if (!$2 | !$3) { + break label$6 + } + $6 = 0; + while (1) { + $4 = 0; + while (1) { + HEAP32[$5 >> 2] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; + $5 = $5 + 4 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + $6 = $6 + 1 | 0; + if (($6 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + case 2: + if (!$2 | !$3) { + break label$6 + } + while (1) { + $4 = 0; + while (1) { + $6 = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($10 << 2) >> 2]; + HEAP8[$5 | 0] = $6; + HEAP8[$5 + 2 | 0] = $6 >>> 16; + HEAP8[$5 + 1 | 0] = $6 >>> 8; + $5 = $5 + 3 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + $10 = $10 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + case 1: + if (!$2 | !$3) { + break label$6 + } + $6 = 0; + while (1) { + $4 = 0; + while (1) { + HEAP16[$5 >> 1] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; + $5 = $5 + 2 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + $6 = $6 + 1 | 0; + if (($6 | 0) != ($3 | 0)) { + continue + } + break; + }; + break label$6; + case 0: + break label$45; + default: + break label$6; + }; + } + if (!$2 | !$3) { + break label$6 + } + $6 = 0; + while (1) { + $4 = 0; + while (1) { + HEAP8[$5 | 0] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; + $5 = $5 + 1 | 0; + $4 = $4 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + $6 = $6 + 1 | 0; + if (($6 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + $2 = HEAP32[$0 + 80 >> 2]; + $1 = $2 + $11 | 0; + HEAP32[$0 + 80 >> 2] = $1; + $3 = HEAP32[$0 + 88 >> 2]; + if ($1 >>> 0 < $2 >>> 0) { + $1 = $0 + 84 | 0; + HEAP32[$1 >> 2] = HEAP32[$1 >> 2] + 1; + } + $4 = 64 - ($2 & 63) | 0; + $1 = ($0 - $4 | 0) - -64 | 0; + label$58 : { + if ($11 >>> 0 < $4 >>> 0) { + memcpy($1, $3, $11); + break label$58; + } + memcpy($1, $3, $4); + $2 = $0 - -64 | 0; + FLAC__MD5Transform($2, $0); + $5 = $3 + $4 | 0; + $1 = $11 - $4 | 0; + if ($1 >>> 0 >= 64) { + while (1) { + $4 = HEAPU8[$5 + 4 | 0] | HEAPU8[$5 + 5 | 0] << 8 | (HEAPU8[$5 + 6 | 0] << 16 | HEAPU8[$5 + 7 | 0] << 24); + $3 = HEAPU8[$5 | 0] | HEAPU8[$5 + 1 | 0] << 8 | (HEAPU8[$5 + 2 | 0] << 16 | HEAPU8[$5 + 3 | 0] << 24); + HEAP8[$0 | 0] = $3; + HEAP8[$0 + 1 | 0] = $3 >>> 8; + HEAP8[$0 + 2 | 0] = $3 >>> 16; + HEAP8[$0 + 3 | 0] = $3 >>> 24; + HEAP8[$0 + 4 | 0] = $4; + HEAP8[$0 + 5 | 0] = $4 >>> 8; + HEAP8[$0 + 6 | 0] = $4 >>> 16; + HEAP8[$0 + 7 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 60 | 0] | HEAPU8[$5 + 61 | 0] << 8 | (HEAPU8[$5 + 62 | 0] << 16 | HEAPU8[$5 + 63 | 0] << 24); + $3 = HEAPU8[$5 + 56 | 0] | HEAPU8[$5 + 57 | 0] << 8 | (HEAPU8[$5 + 58 | 0] << 16 | HEAPU8[$5 + 59 | 0] << 24); + HEAP8[$0 + 56 | 0] = $3; + HEAP8[$0 + 57 | 0] = $3 >>> 8; + HEAP8[$0 + 58 | 0] = $3 >>> 16; + HEAP8[$0 + 59 | 0] = $3 >>> 24; + HEAP8[$0 + 60 | 0] = $4; + HEAP8[$0 + 61 | 0] = $4 >>> 8; + HEAP8[$0 + 62 | 0] = $4 >>> 16; + HEAP8[$0 + 63 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 52 | 0] | HEAPU8[$5 + 53 | 0] << 8 | (HEAPU8[$5 + 54 | 0] << 16 | HEAPU8[$5 + 55 | 0] << 24); + $3 = HEAPU8[$5 + 48 | 0] | HEAPU8[$5 + 49 | 0] << 8 | (HEAPU8[$5 + 50 | 0] << 16 | HEAPU8[$5 + 51 | 0] << 24); + HEAP8[$0 + 48 | 0] = $3; + HEAP8[$0 + 49 | 0] = $3 >>> 8; + HEAP8[$0 + 50 | 0] = $3 >>> 16; + HEAP8[$0 + 51 | 0] = $3 >>> 24; + HEAP8[$0 + 52 | 0] = $4; + HEAP8[$0 + 53 | 0] = $4 >>> 8; + HEAP8[$0 + 54 | 0] = $4 >>> 16; + HEAP8[$0 + 55 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 44 | 0] | HEAPU8[$5 + 45 | 0] << 8 | (HEAPU8[$5 + 46 | 0] << 16 | HEAPU8[$5 + 47 | 0] << 24); + $3 = HEAPU8[$5 + 40 | 0] | HEAPU8[$5 + 41 | 0] << 8 | (HEAPU8[$5 + 42 | 0] << 16 | HEAPU8[$5 + 43 | 0] << 24); + HEAP8[$0 + 40 | 0] = $3; + HEAP8[$0 + 41 | 0] = $3 >>> 8; + HEAP8[$0 + 42 | 0] = $3 >>> 16; + HEAP8[$0 + 43 | 0] = $3 >>> 24; + HEAP8[$0 + 44 | 0] = $4; + HEAP8[$0 + 45 | 0] = $4 >>> 8; + HEAP8[$0 + 46 | 0] = $4 >>> 16; + HEAP8[$0 + 47 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 36 | 0] | HEAPU8[$5 + 37 | 0] << 8 | (HEAPU8[$5 + 38 | 0] << 16 | HEAPU8[$5 + 39 | 0] << 24); + $3 = HEAPU8[$5 + 32 | 0] | HEAPU8[$5 + 33 | 0] << 8 | (HEAPU8[$5 + 34 | 0] << 16 | HEAPU8[$5 + 35 | 0] << 24); + HEAP8[$0 + 32 | 0] = $3; + HEAP8[$0 + 33 | 0] = $3 >>> 8; + HEAP8[$0 + 34 | 0] = $3 >>> 16; + HEAP8[$0 + 35 | 0] = $3 >>> 24; + HEAP8[$0 + 36 | 0] = $4; + HEAP8[$0 + 37 | 0] = $4 >>> 8; + HEAP8[$0 + 38 | 0] = $4 >>> 16; + HEAP8[$0 + 39 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 28 | 0] | HEAPU8[$5 + 29 | 0] << 8 | (HEAPU8[$5 + 30 | 0] << 16 | HEAPU8[$5 + 31 | 0] << 24); + $3 = HEAPU8[$5 + 24 | 0] | HEAPU8[$5 + 25 | 0] << 8 | (HEAPU8[$5 + 26 | 0] << 16 | HEAPU8[$5 + 27 | 0] << 24); + HEAP8[$0 + 24 | 0] = $3; + HEAP8[$0 + 25 | 0] = $3 >>> 8; + HEAP8[$0 + 26 | 0] = $3 >>> 16; + HEAP8[$0 + 27 | 0] = $3 >>> 24; + HEAP8[$0 + 28 | 0] = $4; + HEAP8[$0 + 29 | 0] = $4 >>> 8; + HEAP8[$0 + 30 | 0] = $4 >>> 16; + HEAP8[$0 + 31 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 20 | 0] | HEAPU8[$5 + 21 | 0] << 8 | (HEAPU8[$5 + 22 | 0] << 16 | HEAPU8[$5 + 23 | 0] << 24); + $3 = HEAPU8[$5 + 16 | 0] | HEAPU8[$5 + 17 | 0] << 8 | (HEAPU8[$5 + 18 | 0] << 16 | HEAPU8[$5 + 19 | 0] << 24); + HEAP8[$0 + 16 | 0] = $3; + HEAP8[$0 + 17 | 0] = $3 >>> 8; + HEAP8[$0 + 18 | 0] = $3 >>> 16; + HEAP8[$0 + 19 | 0] = $3 >>> 24; + HEAP8[$0 + 20 | 0] = $4; + HEAP8[$0 + 21 | 0] = $4 >>> 8; + HEAP8[$0 + 22 | 0] = $4 >>> 16; + HEAP8[$0 + 23 | 0] = $4 >>> 24; + $4 = HEAPU8[$5 + 12 | 0] | HEAPU8[$5 + 13 | 0] << 8 | (HEAPU8[$5 + 14 | 0] << 16 | HEAPU8[$5 + 15 | 0] << 24); + $3 = HEAPU8[$5 + 8 | 0] | HEAPU8[$5 + 9 | 0] << 8 | (HEAPU8[$5 + 10 | 0] << 16 | HEAPU8[$5 + 11 | 0] << 24); + HEAP8[$0 + 8 | 0] = $3; + HEAP8[$0 + 9 | 0] = $3 >>> 8; + HEAP8[$0 + 10 | 0] = $3 >>> 16; + HEAP8[$0 + 11 | 0] = $3 >>> 24; + HEAP8[$0 + 12 | 0] = $4; + HEAP8[$0 + 13 | 0] = $4 >>> 8; + HEAP8[$0 + 14 | 0] = $4 >>> 16; + HEAP8[$0 + 15 | 0] = $4 >>> 24; + FLAC__MD5Transform($2, $0); + $5 = $5 - -64 | 0; + $1 = $1 + -64 | 0; + if ($1 >>> 0 > 63) { + continue + } + break; + } + } + memcpy($0, $5, $1); + } + $5 = 1; + } + return $5; + } + + function __stdio_close($0) { + $0 = $0 | 0; + return __wasi_fd_close(HEAP32[$0 + 60 >> 2]) | 0; + } + + function __wasi_syscall_ret($0) { + if (!$0) { + return 0 + } + HEAP32[2896] = $0; + return -1; + } + + function __stdio_read($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0, $5 = 0, $6 = 0; + $3 = global$0 - 32 | 0; + global$0 = $3; + HEAP32[$3 + 16 >> 2] = $1; + $4 = HEAP32[$0 + 48 >> 2]; + HEAP32[$3 + 20 >> 2] = $2 - (($4 | 0) != 0); + $5 = HEAP32[$0 + 44 >> 2]; + HEAP32[$3 + 28 >> 2] = $4; + HEAP32[$3 + 24 >> 2] = $5; + label$1 : { + label$2 : { + label$3 : { + if (__wasi_syscall_ret(__wasi_fd_read(HEAP32[$0 + 60 >> 2], $3 + 16 | 0, 2, $3 + 12 | 0) | 0)) { + HEAP32[$3 + 12 >> 2] = -1; + $2 = -1; + break label$3; + } + $4 = HEAP32[$3 + 12 >> 2]; + if (($4 | 0) > 0) { + break label$2 + } + $2 = $4; + } + HEAP32[$0 >> 2] = HEAP32[$0 >> 2] | $2 & 48 ^ 16; + break label$1; + } + $6 = HEAP32[$3 + 20 >> 2]; + if ($4 >>> 0 <= $6 >>> 0) { + $2 = $4; + break label$1; + } + $5 = HEAP32[$0 + 44 >> 2]; + HEAP32[$0 + 4 >> 2] = $5; + HEAP32[$0 + 8 >> 2] = $5 + ($4 - $6 | 0); + if (!HEAP32[$0 + 48 >> 2]) { + break label$1 + } + HEAP32[$0 + 4 >> 2] = $5 + 1; + HEAP8[($1 + $2 | 0) + -1 | 0] = HEAPU8[$5 | 0]; + } + global$0 = $3 + 32 | 0; + return $2 | 0; + } + + function __stdio_seek($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + var $4 = 0; + $4 = global$0 - 16 | 0; + global$0 = $4; + label$1 : { + if (!__wasi_syscall_ret(legalimport$__wasi_fd_seek(HEAP32[$0 + 60 >> 2], $1 | 0, $2 | 0, $3 & 255, $4 + 8 | 0) | 0)) { + $1 = HEAP32[$4 + 12 >> 2]; + $0 = HEAP32[$4 + 8 >> 2]; + break label$1; + } + HEAP32[$4 + 8 >> 2] = -1; + HEAP32[$4 + 12 >> 2] = -1; + $1 = -1; + $0 = -1; + } + global$0 = $4 + 16 | 0; + i64toi32_i32$HIGH_BITS = $1; + return $0 | 0; + } + + function fflush($0) { + var $1 = 0; + if ($0) { + if (HEAP32[$0 + 76 >> 2] <= -1) { + return __fflush_unlocked($0) + } + return __fflush_unlocked($0); + } + if (HEAP32[2794]) { + $1 = fflush(HEAP32[2794]) + } + $0 = HEAP32[3023]; + if ($0) { + while (1) { + if (HEAPU32[$0 + 20 >> 2] > HEAPU32[$0 + 28 >> 2]) { + $1 = __fflush_unlocked($0) | $1 + } + $0 = HEAP32[$0 + 56 >> 2]; + if ($0) { + continue + } + break; + } + } + return $1; + } + + function __fflush_unlocked($0) { + var $1 = 0, $2 = 0; + label$1 : { + if (HEAPU32[$0 + 20 >> 2] <= HEAPU32[$0 + 28 >> 2]) { + break label$1 + } + FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($0, 0, 0) | 0; + if (HEAP32[$0 + 20 >> 2]) { + break label$1 + } + return -1; + } + $1 = HEAP32[$0 + 4 >> 2]; + $2 = HEAP32[$0 + 8 >> 2]; + if ($1 >>> 0 < $2 >>> 0) { + $1 = $1 - $2 | 0; + FUNCTION_TABLE[HEAP32[$0 + 40 >> 2]]($0, $1, $1 >> 31, 1) | 0; + } + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + return 0; + } + + function fclose($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0; + $4 = HEAP32[$0 + 76 >> 2] >= 0 ? 1 : 0; + $3 = HEAP32[$0 >> 2] & 1; + if (!$3) { + $1 = HEAP32[$0 + 52 >> 2]; + if ($1) { + HEAP32[$1 + 56 >> 2] = HEAP32[$0 + 56 >> 2] + } + $2 = HEAP32[$0 + 56 >> 2]; + if ($2) { + HEAP32[$2 + 52 >> 2] = $1 + } + if (HEAP32[3023] == ($0 | 0)) { + HEAP32[3023] = $2 + } + } + fflush($0); + FUNCTION_TABLE[HEAP32[$0 + 12 >> 2]]($0) | 0; + $1 = HEAP32[$0 + 96 >> 2]; + if ($1) { + dlfree($1) + } + label$7 : { + if (!$3) { + dlfree($0); + break label$7; + } + if (!$4) { + break label$7 + } + } + } + + function memcmp($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0; + label$1 : { + if (!$2) { + break label$1 + } + while (1) { + $3 = HEAPU8[$0 | 0]; + $4 = HEAPU8[$1 | 0]; + if (($3 | 0) == ($4 | 0)) { + $1 = $1 + 1 | 0; + $0 = $0 + 1 | 0; + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break label$1; + } + break; + }; + $5 = $3 - $4 | 0; + } + return $5; + } + + function FLAC__cpu_info($0) { + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 3; + HEAP32[$0 + 56 >> 2] = 0; + HEAP32[$0 + 60 >> 2] = 0; + HEAP32[$0 + 48 >> 2] = 0; + HEAP32[$0 + 52 >> 2] = 0; + HEAP32[$0 + 40 >> 2] = 0; + HEAP32[$0 + 44 >> 2] = 0; + HEAP32[$0 + 32 >> 2] = 0; + HEAP32[$0 + 36 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + } + + function lround($0) { + $0 = +round(+$0); + if (Math_abs($0) < 2147483648.0) { + return ~~$0 + } + return -2147483648; + } + + function log($0) { + var $1 = 0, $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0, $6 = 0, $7 = 0.0, $8 = 0.0, $9 = 0.0, $10 = 0.0; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + wasm2js_scratch_store_f64(+$0); + $1 = wasm2js_scratch_load_i32(1) | 0; + $3 = wasm2js_scratch_load_i32(0) | 0; + if (($1 | 0) > 0 ? 1 : ($1 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { + $5 = $1; + if ($1 >>> 0 > 1048575) { + break label$4 + } + } + if (!($1 & 2147483647 | $3)) { + return -1.0 / ($0 * $0) + } + if (($1 | 0) > -1 ? 1 : 0) { + break label$3 + } + return ($0 - $0) / 0.0; + } + if ($5 >>> 0 > 2146435071) { + break label$1 + } + $1 = 1072693248; + $6 = -1023; + if (($5 | 0) != 1072693248) { + $1 = $5; + break label$2; + } + if ($3) { + break label$2 + } + return 0.0; + } + wasm2js_scratch_store_f64(+($0 * 18014398509481984.0)); + $1 = wasm2js_scratch_load_i32(1) | 0; + $3 = wasm2js_scratch_load_i32(0) | 0; + $6 = -1077; + } + $1 = $1 + 614242 | 0; + $4 = +(($1 >>> 20 | 0) + $6 | 0); + wasm2js_scratch_store_i32(0, $3 | 0); + wasm2js_scratch_store_i32(1, ($1 & 1048575) + 1072079006 | 0); + $0 = +wasm2js_scratch_load_f64() + -1.0; + $2 = $0 / ($0 + 2.0); + $7 = $4 * .6931471803691238; + $8 = $0; + $9 = $4 * 1.9082149292705877e-10; + $10 = $2; + $4 = $0 * ($0 * .5); + $2 = $2 * $2; + $0 = $2 * $2; + $0 = $7 + ($8 + ($9 + $10 * ($4 + ($0 * ($0 * ($0 * .15313837699209373 + .22222198432149784) + .3999999999940942) + $2 * ($0 * ($0 * ($0 * .14798198605116586 + .1818357216161805) + .2857142874366239) + .6666666666666735))) - $4)); + } + return $0; + } + + function FLAC__lpc_window_data($0, $1, $2, $3) { + var $4 = 0, $5 = 0; + if ($3) { + while (1) { + $5 = $4 << 2; + HEAPF32[$5 + $2 >> 2] = HEAPF32[$1 + $5 >> 2] * Math_fround(HEAP32[$0 + $5 >> 2]); + $4 = $4 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + } + } + } + + function FLAC__lpc_compute_autocorrelation($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + var $4 = 0, $5 = 0, $6 = 0, $7 = Math_fround(0), $8 = 0, $9 = 0; + $6 = $1 - $2 | 0; + label$1 : { + if (!$2) { + while (1) { + $4 = $4 + 1 | 0; + if ($4 >>> 0 <= $6 >>> 0) { + continue + } + break; + }; + break label$1; + } + $9 = memset($3, $2 << 2); + while (1) { + $7 = HEAPF32[($4 << 2) + $0 >> 2]; + $5 = 0; + while (1) { + $8 = ($5 << 2) + $9 | 0; + HEAPF32[$8 >> 2] = HEAPF32[$8 >> 2] + Math_fround($7 * HEAPF32[($4 + $5 << 2) + $0 >> 2]); + $5 = $5 + 1 | 0; + if (($5 | 0) != ($2 | 0)) { + continue + } + break; + }; + $4 = $4 + 1 | 0; + if ($4 >>> 0 <= $6 >>> 0) { + continue + } + break; + }; + } + if ($4 >>> 0 < $1 >>> 0) { + while (1) { + $2 = $1 - $4 | 0; + if ($2) { + $7 = HEAPF32[($4 << 2) + $0 >> 2]; + $5 = 0; + while (1) { + $6 = ($5 << 2) + $3 | 0; + HEAPF32[$6 >> 2] = HEAPF32[$6 >> 2] + Math_fround($7 * HEAPF32[($4 + $5 << 2) + $0 >> 2]); + $5 = $5 + 1 | 0; + if ($5 >>> 0 < $2 >>> 0) { + continue + } + break; + }; + } + $4 = $4 + 1 | 0; + if (($4 | 0) != ($1 | 0)) { + continue + } + break; + } + } + } + + function FLAC__lpc_compute_lp_coefficients($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0.0, $7 = 0, $8 = 0, $9 = 0.0, $10 = 0.0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; + $7 = global$0 - 256 | 0; + global$0 = $7; + $13 = HEAP32[$1 >> 2]; + $10 = +HEAPF32[$0 >> 2]; + label$1 : { + while (1) { + if (($5 | 0) == ($13 | 0)) { + break label$1 + } + $11 = $5 + 1 | 0; + $6 = +Math_fround(-HEAPF32[($11 << 2) + $0 >> 2]); + label$3 : { + if ($5) { + $12 = $5 >>> 1 | 0; + $4 = 0; + while (1) { + $6 = $6 - HEAPF64[($4 << 3) + $7 >> 3] * +HEAPF32[($5 - $4 << 2) + $0 >> 2]; + $4 = $4 + 1 | 0; + if (($5 | 0) != ($4 | 0)) { + continue + } + break; + }; + $6 = $6 / $10; + HEAPF64[($5 << 3) + $7 >> 3] = $6; + $4 = 0; + if ($12) { + while (1) { + $8 = ($4 << 3) + $7 | 0; + $9 = HEAPF64[$8 >> 3]; + $14 = $8; + $8 = (($4 ^ -1) + $5 << 3) + $7 | 0; + HEAPF64[$14 >> 3] = $9 + $6 * HEAPF64[$8 >> 3]; + HEAPF64[$8 >> 3] = $6 * $9 + HEAPF64[$8 >> 3]; + $4 = $4 + 1 | 0; + if (($12 | 0) != ($4 | 0)) { + continue + } + break; + } + } + if (!($5 & 1)) { + break label$3 + } + $8 = ($12 << 3) + $7 | 0; + $9 = HEAPF64[$8 >> 3]; + HEAPF64[$8 >> 3] = $9 + $6 * $9; + break label$3; + } + $6 = $6 / $10; + HEAPF64[($5 << 3) + $7 >> 3] = $6; + } + $9 = 1.0 - $6 * $6; + $4 = 0; + while (1) { + HEAPF32[(($5 << 7) + $2 | 0) + ($4 << 2) >> 2] = -Math_fround(HEAPF64[($4 << 3) + $7 >> 3]); + $4 = $4 + 1 | 0; + if ($4 >>> 0 <= $5 >>> 0) { + continue + } + break; + }; + $10 = $10 * $9; + HEAPF64[($5 << 3) + $3 >> 3] = $10; + $5 = $11; + if ($10 != 0.0) { + continue + } + break; + }; + HEAP32[$1 >> 2] = $11; + } + global$0 = $7 + 256 | 0; + } + + function FLAC__lpc_quantize_coefficients($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0.0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0.0, $12 = 0, $13 = 0, $14 = Math_fround(0); + $8 = global$0 - 16 | 0; + global$0 = $8; + label$1 : { + if (!$1) { + $7 = 2; + break label$1; + } + $5 = $2 + -1 | 0; + $2 = 0; + while (1) { + $11 = +Math_fround(Math_abs(HEAPF32[($2 << 2) + $0 >> 2])); + $6 = $6 < $11 ? $11 : $6; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + $7 = 2; + if ($6 <= 0.0) { + break label$1 + } + $9 = 1 << $5; + $12 = $9 + -1 | 0; + $10 = 0 - $9 | 0; + frexp($6, $8 + 12 | 0); + $2 = HEAP32[$8 + 12 >> 2]; + HEAP32[$8 + 12 >> 2] = $2 + -1; + $5 = $5 - $2 | 0; + HEAP32[$4 >> 2] = $5; + label$4 : { + $7 = -1 << HEAP32[1413] + -1; + $2 = $7 ^ -1; + if (($5 | 0) > ($2 | 0)) { + HEAP32[$4 >> 2] = $2; + $5 = $2; + break label$4; + } + if (($5 | 0) >= ($7 | 0)) { + break label$4 + } + $7 = 1; + break label$1; + } + $7 = 0; + if (($5 | 0) >= 0) { + if (!$1) { + break label$1 + } + $6 = 0.0; + $2 = 0; + while (1) { + $13 = $2 << 2; + $6 = $6 + +Math_fround(HEAPF32[$13 + $0 >> 2] * Math_fround(1 << $5)); + $5 = lround($6); + $5 = ($5 | 0) < ($9 | 0) ? (($5 | 0) < ($10 | 0) ? $10 : $5) : $12; + HEAP32[$3 + $13 >> 2] = $5; + $2 = $2 + 1 | 0; + if (($2 | 0) == ($1 | 0)) { + break label$1 + } + $6 = $6 - +($5 | 0); + $5 = HEAP32[$4 >> 2]; + continue; + }; + } + if ($1) { + $2 = 0; + $14 = Math_fround(1 << 0 - $5); + $6 = 0.0; + while (1) { + $7 = $2 << 2; + $6 = $6 + +Math_fround(HEAPF32[$7 + $0 >> 2] / $14); + $5 = lround($6); + $5 = ($5 | 0) < ($9 | 0) ? (($5 | 0) < ($10 | 0) ? $10 : $5) : $12; + HEAP32[$3 + $7 >> 2] = $5; + $6 = $6 - +($5 | 0); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + $7 = 0; + HEAP32[$4 >> 2] = 0; + } + global$0 = $8 + 16 | 0; + return $7; + } + + function FLAC__lpc_compute_residual_from_qlp_coefficients($0, $1, $2, $3, $4, $5) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0; + label$1 : { + if ($3 >>> 0 >= 13) { + if (($1 | 0) < 1) { + break label$1 + } + $25 = $3 + -13 | 0; + while (1) { + $17 = 0; + $20 = 0; + $19 = 0; + $22 = 0; + $21 = 0; + $24 = 0; + $23 = 0; + $26 = 0; + $18 = 0; + $16 = 0; + $15 = 0; + $14 = 0; + $13 = 0; + $12 = 0; + $11 = 0; + $10 = 0; + $9 = 0; + $8 = 0; + $7 = 0; + $3 = 0; + label$4 : { + switch ($25 | 0) { + case 19: + $17 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -128 >> 2], HEAP32[$2 + 124 >> 2]); + case 18: + $20 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -124 >> 2], HEAP32[$2 + 120 >> 2]) + $17 | 0; + case 17: + $19 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -120 >> 2], HEAP32[$2 + 116 >> 2]) + $20 | 0; + case 16: + $22 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -116 >> 2], HEAP32[$2 + 112 >> 2]) + $19 | 0; + case 15: + $21 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -112 >> 2], HEAP32[$2 + 108 >> 2]) + $22 | 0; + case 14: + $24 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -108 >> 2], HEAP32[$2 + 104 >> 2]) + $21 | 0; + case 13: + $23 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -104 >> 2], HEAP32[$2 + 100 >> 2]) + $24 | 0; + case 12: + $26 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -100 >> 2], HEAP32[$2 + 96 >> 2]) + $23 | 0; + case 11: + $18 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -96 >> 2], HEAP32[$2 + 92 >> 2]) + $26 | 0; + case 10: + $16 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -92 >> 2], HEAP32[$2 + 88 >> 2]) + $18 | 0; + case 9: + $15 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -88 >> 2], HEAP32[$2 + 84 >> 2]) + $16 | 0; + case 8: + $14 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -84 >> 2], HEAP32[$2 + 80 >> 2]) + $15 | 0; + case 7: + $13 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -80 >> 2], HEAP32[$2 + 76 >> 2]) + $14 | 0; + case 6: + $12 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -76 >> 2], HEAP32[$2 + 72 >> 2]) + $13 | 0; + case 5: + $11 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -72 >> 2], HEAP32[$2 + 68 >> 2]) + $12 | 0; + case 4: + $10 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -68 >> 2], HEAP32[$2 + 64 >> 2]) + $11 | 0; + case 3: + $9 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -64 >> 2], HEAP32[$2 + 60 >> 2]) + $10 | 0; + case 2: + $8 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -60 >> 2], HEAP32[$2 + 56 >> 2]) + $9 | 0; + case 1: + $7 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -56 >> 2], HEAP32[$2 + 52 >> 2]) + $8 | 0; + case 0: + $3 = ($6 << 2) + $0 | 0; + $3 = ((((((((((((Math_imul(HEAP32[$3 + -52 >> 2], HEAP32[$2 + 48 >> 2]) + $7 | 0) + Math_imul(HEAP32[$3 + -48 >> 2], HEAP32[$2 + 44 >> 2]) | 0) + Math_imul(HEAP32[$3 + -44 >> 2], HEAP32[$2 + 40 >> 2]) | 0) + Math_imul(HEAP32[$3 + -40 >> 2], HEAP32[$2 + 36 >> 2]) | 0) + Math_imul(HEAP32[$3 + -36 >> 2], HEAP32[$2 + 32 >> 2]) | 0) + Math_imul(HEAP32[$3 + -32 >> 2], HEAP32[$2 + 28 >> 2]) | 0) + Math_imul(HEAP32[$3 + -28 >> 2], HEAP32[$2 + 24 >> 2]) | 0) + Math_imul(HEAP32[$3 + -24 >> 2], HEAP32[$2 + 20 >> 2]) | 0) + Math_imul(HEAP32[$3 + -20 >> 2], HEAP32[$2 + 16 >> 2]) | 0) + Math_imul(HEAP32[$3 + -16 >> 2], HEAP32[$2 + 12 >> 2]) | 0) + Math_imul(HEAP32[$3 + -12 >> 2], HEAP32[$2 + 8 >> 2]) | 0) + Math_imul(HEAP32[$3 + -8 >> 2], HEAP32[$2 + 4 >> 2]) | 0) + Math_imul(HEAP32[$3 + -4 >> 2], HEAP32[$2 >> 2]) | 0; + break; + default: + break label$4; + }; + } + $7 = $6 << 2; + HEAP32[$7 + $5 >> 2] = HEAP32[$0 + $7 >> 2] - ($3 >> $4); + $6 = $6 + 1 | 0; + if (($6 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 9) { + if ($3 >>> 0 >= 11) { + if (($3 | 0) != 12) { + if (($1 | 0) < 1) { + break label$1 + } + $15 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $10 = HEAP32[$0 + -28 >> 2]; + $11 = HEAP32[$0 + -32 >> 2]; + $12 = HEAP32[$0 + -36 >> 2]; + $13 = HEAP32[$0 + -40 >> 2]; + $16 = HEAP32[$0 + -44 >> 2]; + $18 = HEAP32[$2 >> 2]; + $17 = HEAP32[$2 + 4 >> 2]; + $20 = HEAP32[$2 + 8 >> 2]; + $19 = HEAP32[$2 + 12 >> 2]; + $22 = HEAP32[$2 + 16 >> 2]; + $21 = HEAP32[$2 + 20 >> 2]; + $24 = HEAP32[$2 + 24 >> 2]; + $23 = HEAP32[$2 + 28 >> 2]; + $26 = HEAP32[$2 + 32 >> 2]; + $25 = HEAP32[$2 + 36 >> 2]; + $28 = HEAP32[$2 + 40 >> 2]; + $2 = 0; + while (1) { + $14 = $13; + $13 = $12; + $12 = $11; + $11 = $10; + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $15; + $27 = $2 << 2; + $15 = HEAP32[$27 + $0 >> 2]; + HEAP32[$5 + $27 >> 2] = $15 - ((((((((((Math_imul($14, $25) + Math_imul($16, $28) | 0) + Math_imul($13, $26) | 0) + Math_imul($12, $23) | 0) + Math_imul($11, $24) | 0) + Math_imul($10, $21) | 0) + Math_imul($9, $22) | 0) + Math_imul($8, $19) | 0) + Math_imul($7, $20) | 0) + Math_imul($3, $17) | 0) + Math_imul($6, $18) >> $4); + $16 = $14; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $16 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $10 = HEAP32[$0 + -28 >> 2]; + $11 = HEAP32[$0 + -32 >> 2]; + $12 = HEAP32[$0 + -36 >> 2]; + $13 = HEAP32[$0 + -40 >> 2]; + $14 = HEAP32[$0 + -44 >> 2]; + $18 = HEAP32[$0 + -48 >> 2]; + $17 = HEAP32[$2 >> 2]; + $20 = HEAP32[$2 + 4 >> 2]; + $19 = HEAP32[$2 + 8 >> 2]; + $22 = HEAP32[$2 + 12 >> 2]; + $21 = HEAP32[$2 + 16 >> 2]; + $24 = HEAP32[$2 + 20 >> 2]; + $23 = HEAP32[$2 + 24 >> 2]; + $26 = HEAP32[$2 + 28 >> 2]; + $25 = HEAP32[$2 + 32 >> 2]; + $28 = HEAP32[$2 + 36 >> 2]; + $27 = HEAP32[$2 + 40 >> 2]; + $30 = HEAP32[$2 + 44 >> 2]; + $2 = 0; + while (1) { + $15 = $14; + $14 = $13; + $13 = $12; + $12 = $11; + $11 = $10; + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $16; + $29 = $2 << 2; + $16 = HEAP32[$29 + $0 >> 2]; + HEAP32[$5 + $29 >> 2] = $16 - (((((((((((Math_imul($15, $27) + Math_imul($18, $30) | 0) + Math_imul($14, $28) | 0) + Math_imul($13, $25) | 0) + Math_imul($12, $26) | 0) + Math_imul($11, $23) | 0) + Math_imul($10, $24) | 0) + Math_imul($9, $21) | 0) + Math_imul($8, $22) | 0) + Math_imul($7, $19) | 0) + Math_imul($3, $20) | 0) + Math_imul($6, $17) >> $4); + $18 = $15; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 10) { + if (($1 | 0) < 1) { + break label$1 + } + $13 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $10 = HEAP32[$0 + -28 >> 2]; + $11 = HEAP32[$0 + -32 >> 2]; + $14 = HEAP32[$0 + -36 >> 2]; + $16 = HEAP32[$2 >> 2]; + $15 = HEAP32[$2 + 4 >> 2]; + $18 = HEAP32[$2 + 8 >> 2]; + $17 = HEAP32[$2 + 12 >> 2]; + $20 = HEAP32[$2 + 16 >> 2]; + $19 = HEAP32[$2 + 20 >> 2]; + $22 = HEAP32[$2 + 24 >> 2]; + $21 = HEAP32[$2 + 28 >> 2]; + $24 = HEAP32[$2 + 32 >> 2]; + $2 = 0; + while (1) { + $12 = $11; + $11 = $10; + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $13; + $23 = $2 << 2; + $13 = HEAP32[$23 + $0 >> 2]; + HEAP32[$5 + $23 >> 2] = $13 - ((((((((Math_imul($12, $21) + Math_imul($14, $24) | 0) + Math_imul($11, $22) | 0) + Math_imul($10, $19) | 0) + Math_imul($9, $20) | 0) + Math_imul($8, $17) | 0) + Math_imul($7, $18) | 0) + Math_imul($3, $15) | 0) + Math_imul($6, $16) >> $4); + $14 = $12; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $14 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $10 = HEAP32[$0 + -28 >> 2]; + $11 = HEAP32[$0 + -32 >> 2]; + $12 = HEAP32[$0 + -36 >> 2]; + $15 = HEAP32[$0 + -40 >> 2]; + $16 = HEAP32[$2 >> 2]; + $18 = HEAP32[$2 + 4 >> 2]; + $17 = HEAP32[$2 + 8 >> 2]; + $20 = HEAP32[$2 + 12 >> 2]; + $19 = HEAP32[$2 + 16 >> 2]; + $22 = HEAP32[$2 + 20 >> 2]; + $21 = HEAP32[$2 + 24 >> 2]; + $24 = HEAP32[$2 + 28 >> 2]; + $23 = HEAP32[$2 + 32 >> 2]; + $26 = HEAP32[$2 + 36 >> 2]; + $2 = 0; + while (1) { + $13 = $12; + $12 = $11; + $11 = $10; + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $14; + $25 = $2 << 2; + $14 = HEAP32[$25 + $0 >> 2]; + HEAP32[$5 + $25 >> 2] = $14 - (((((((((Math_imul($13, $23) + Math_imul($15, $26) | 0) + Math_imul($12, $24) | 0) + Math_imul($11, $21) | 0) + Math_imul($10, $22) | 0) + Math_imul($9, $19) | 0) + Math_imul($8, $20) | 0) + Math_imul($7, $17) | 0) + Math_imul($3, $18) | 0) + Math_imul($6, $16) >> $4); + $15 = $13; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 5) { + if ($3 >>> 0 >= 7) { + if (($3 | 0) != 8) { + if (($1 | 0) < 1) { + break label$1 + } + $11 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $12 = HEAP32[$0 + -28 >> 2]; + $13 = HEAP32[$2 >> 2]; + $14 = HEAP32[$2 + 4 >> 2]; + $16 = HEAP32[$2 + 8 >> 2]; + $15 = HEAP32[$2 + 12 >> 2]; + $18 = HEAP32[$2 + 16 >> 2]; + $17 = HEAP32[$2 + 20 >> 2]; + $20 = HEAP32[$2 + 24 >> 2]; + $2 = 0; + while (1) { + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $11; + $19 = $2 << 2; + $11 = HEAP32[$19 + $0 >> 2]; + HEAP32[$5 + $19 >> 2] = $11 - ((((((Math_imul($10, $17) + Math_imul($12, $20) | 0) + Math_imul($9, $18) | 0) + Math_imul($8, $15) | 0) + Math_imul($7, $16) | 0) + Math_imul($3, $14) | 0) + Math_imul($6, $13) >> $4); + $12 = $10; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $12 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $10 = HEAP32[$0 + -28 >> 2]; + $13 = HEAP32[$0 + -32 >> 2]; + $14 = HEAP32[$2 >> 2]; + $16 = HEAP32[$2 + 4 >> 2]; + $15 = HEAP32[$2 + 8 >> 2]; + $18 = HEAP32[$2 + 12 >> 2]; + $17 = HEAP32[$2 + 16 >> 2]; + $20 = HEAP32[$2 + 20 >> 2]; + $19 = HEAP32[$2 + 24 >> 2]; + $22 = HEAP32[$2 + 28 >> 2]; + $2 = 0; + while (1) { + $11 = $10; + $10 = $9; + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $12; + $21 = $2 << 2; + $12 = HEAP32[$21 + $0 >> 2]; + HEAP32[$5 + $21 >> 2] = $12 - (((((((Math_imul($11, $19) + Math_imul($13, $22) | 0) + Math_imul($10, $20) | 0) + Math_imul($9, $17) | 0) + Math_imul($8, $18) | 0) + Math_imul($7, $15) | 0) + Math_imul($3, $16) | 0) + Math_imul($6, $14) >> $4); + $13 = $11; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 6) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $10 = HEAP32[$0 + -20 >> 2]; + $11 = HEAP32[$2 >> 2]; + $12 = HEAP32[$2 + 4 >> 2]; + $13 = HEAP32[$2 + 8 >> 2]; + $14 = HEAP32[$2 + 12 >> 2]; + $16 = HEAP32[$2 + 16 >> 2]; + $2 = 0; + while (1) { + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $9; + $15 = $2 << 2; + $9 = HEAP32[$15 + $0 >> 2]; + HEAP32[$5 + $15 >> 2] = $9 - ((((Math_imul($8, $14) + Math_imul($10, $16) | 0) + Math_imul($7, $13) | 0) + Math_imul($3, $12) | 0) + Math_imul($6, $11) >> $4); + $10 = $8; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $10 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $7 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $11 = HEAP32[$0 + -24 >> 2]; + $12 = HEAP32[$2 >> 2]; + $13 = HEAP32[$2 + 4 >> 2]; + $14 = HEAP32[$2 + 8 >> 2]; + $16 = HEAP32[$2 + 12 >> 2]; + $15 = HEAP32[$2 + 16 >> 2]; + $18 = HEAP32[$2 + 20 >> 2]; + $2 = 0; + while (1) { + $9 = $8; + $8 = $7; + $7 = $3; + $3 = $6; + $6 = $10; + $17 = $2 << 2; + $10 = HEAP32[$17 + $0 >> 2]; + HEAP32[$5 + $17 >> 2] = $10 - (((((Math_imul($9, $15) + Math_imul($11, $18) | 0) + Math_imul($8, $16) | 0) + Math_imul($7, $14) | 0) + Math_imul($3, $13) | 0) + Math_imul($6, $12) >> $4); + $11 = $9; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 3) { + if (($3 | 0) != 4) { + if (($1 | 0) < 1) { + break label$1 + } + $7 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $8 = HEAP32[$0 + -12 >> 2]; + $9 = HEAP32[$2 >> 2]; + $10 = HEAP32[$2 + 4 >> 2]; + $11 = HEAP32[$2 + 8 >> 2]; + $2 = 0; + while (1) { + $3 = $6; + $6 = $7; + $12 = $2 << 2; + $7 = HEAP32[$12 + $0 >> 2]; + HEAP32[$5 + $12 >> 2] = $7 - ((Math_imul($3, $10) + Math_imul($8, $11) | 0) + Math_imul($6, $9) >> $4); + $8 = $3; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $8 = HEAP32[$0 + -4 >> 2]; + $6 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $9 = HEAP32[$0 + -16 >> 2]; + $10 = HEAP32[$2 >> 2]; + $11 = HEAP32[$2 + 4 >> 2]; + $12 = HEAP32[$2 + 8 >> 2]; + $13 = HEAP32[$2 + 12 >> 2]; + $2 = 0; + while (1) { + $7 = $3; + $3 = $6; + $6 = $8; + $14 = $2 << 2; + $8 = HEAP32[$14 + $0 >> 2]; + HEAP32[$5 + $14 >> 2] = $8 - (((Math_imul($7, $12) + Math_imul($9, $13) | 0) + Math_imul($3, $11) | 0) + Math_imul($6, $10) >> $4); + $9 = $7; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 2) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$0 + -4 >> 2]; + $3 = HEAP32[$2 >> 2]; + $2 = 0; + while (1) { + $7 = Math_imul($3, $6); + $8 = $2 << 2; + $6 = HEAP32[$8 + $0 >> 2]; + HEAP32[$5 + $8 >> 2] = $6 - ($7 >> $4); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $3 = HEAP32[$0 + -4 >> 2]; + $7 = HEAP32[$0 + -8 >> 2]; + $8 = HEAP32[$2 >> 2]; + $9 = HEAP32[$2 + 4 >> 2]; + $2 = 0; + while (1) { + $6 = $3; + $10 = $2 << 2; + $3 = HEAP32[$10 + $0 >> 2]; + HEAP32[$5 + $10 >> 2] = $3 - (Math_imul($6, $8) + Math_imul($7, $9) >> $4); + $7 = $6; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__lpc_compute_residual_from_qlp_coefficients_wide($0, $1, $2, $3, $4, $5) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0; + label$1 : { + if ($3 >>> 0 >= 13) { + if (($1 | 0) < 1) { + break label$1 + } + $18 = $4; + $12 = $3 + -13 | 0; + while (1) { + $4 = 0; + $3 = 0; + label$4 : { + switch ($12 | 0) { + case 19: + $3 = HEAP32[(($15 << 2) + $0 | 0) + -128 >> 2]; + $4 = $3; + $7 = $3 >> 31; + $3 = HEAP32[$2 + 124 >> 2]; + $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); + $3 = i64toi32_i32$HIGH_BITS; + case 18: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -124 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 120 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 17: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -120 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 116 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 16: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -116 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 112 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 15: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -112 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 108 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 14: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -108 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 104 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 13: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -104 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 100 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 12: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -100 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 96 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 11: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -96 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 92 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 10: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -92 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 88 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 9: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -88 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 84 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 8: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -84 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 80 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 7: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -80 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 76 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 6: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -76 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 72 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 5: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -72 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 68 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 4: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -68 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 64 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 3: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -64 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 60 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 2: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -60 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 56 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 1: + $7 = HEAP32[(($15 << 2) + $0 | 0) + -56 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 52 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 0: + $8 = ($15 << 2) + $0 | 0; + $7 = HEAP32[$8 + -52 >> 2]; + $6 = $7; + $9 = $7 >> 31; + $7 = HEAP32[$2 + 48 >> 2]; + $7 = __wasm_i64_mul($6, $9, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -48 >> 2]; + $4 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 44 >> 2]; + $3 = __wasm_i64_mul($4, $9, $3, $3 >> 31); + $4 = $3 + $7 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -44 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 40 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -40 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 36 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -36 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 32 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -32 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 28 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -28 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 24 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -24 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 20 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -20 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 16 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -16 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 12 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -12 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 8 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -8 >> 2]; + $7 = $3; + $9 = $3 >> 31; + $3 = HEAP32[$2 + 4 >> 2]; + $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$8 + -4 >> 2]; + $7 = $3; + $8 = $3 >> 31; + $3 = HEAP32[$2 >> 2]; + $3 = __wasm_i64_mul($7, $8, $3, $3 >> 31); + $4 = $3 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; + $3 = $6; + break; + default: + break label$4; + }; + } + $7 = $15 << 2; + $6 = $7 + $5 | 0; + $9 = HEAP32[$0 + $7 >> 2]; + $7 = $3; + $3 = $18; + $8 = $3 & 31; + HEAP32[$6 >> 2] = $9 - (32 <= ($3 & 63) >>> 0 ? $7 >> $8 : ((1 << $8) - 1 & $7) << 32 - $8 | $4 >>> $8); + $15 = $15 + 1 | 0; + if (($15 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 9) { + if ($3 >>> 0 >= 11) { + if (($3 | 0) != 12) { + if (($1 | 0) < 1) { + break label$1 + } + $10 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $8 = HEAP32[$0 + -28 >> 2]; + $9 = HEAP32[$0 + -32 >> 2]; + $11 = HEAP32[$0 + -36 >> 2]; + $17 = HEAP32[$0 + -40 >> 2]; + $13 = HEAP32[$0 + -44 >> 2]; + $6 = HEAP32[$2 >> 2]; + $40 = $6; + $41 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $42 = $6; + $37 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $38 = $6; + $39 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $34 = $6; + $35 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $36 = $6; + $31 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $32 = $6; + $33 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $29 = $6; + $30 = $6 >> 31; + $6 = HEAP32[$2 + 28 >> 2]; + $26 = $6; + $27 = $6 >> 31; + $6 = HEAP32[$2 + 32 >> 2]; + $28 = $6; + $23 = $6 >> 31; + $6 = HEAP32[$2 + 36 >> 2]; + $24 = $6; + $25 = $6 >> 31; + $2 = HEAP32[$2 + 40 >> 2]; + $21 = $2; + $22 = $2 >> 31; + $2 = 0; + while (1) { + $16 = $17; + $17 = $11; + $11 = $9; + $9 = $8; + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $10; + $6 = $2 << 2; + $20 = $6 + $5 | 0; + $10 = HEAP32[$0 + $6 >> 2]; + $14 = __wasm_i64_mul($16, $16 >> 31, $24, $25); + $6 = i64toi32_i32$HIGH_BITS; + $13 = __wasm_i64_mul($13, $13 >> 31, $21, $22); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($17, $17 >> 31, $28, $23); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($11, $11 >> 31, $26, $27); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($9, $9 >> 31, $29, $30); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($8, $8 >> 31, $32, $33); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($12, $12 >> 31, $36, $31); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($7, $7 >> 31, $34, $35); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($18, $18 >> 31, $38, $39); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($3, $3 >> 31, $42, $37); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = __wasm_i64_mul($15, $15 >> 31, $40, $41); + $14 = $13 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $13 = $6; + $6 = $4; + $19 = $6 & 31; + HEAP32[$20 >> 2] = $10 - (32 <= ($6 & 63) >>> 0 ? $13 >> $19 : ((1 << $19) - 1 & $13) << 32 - $19 | $14 >>> $19); + $13 = $16; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $13 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $8 = HEAP32[$0 + -28 >> 2]; + $9 = HEAP32[$0 + -32 >> 2]; + $11 = HEAP32[$0 + -36 >> 2]; + $17 = HEAP32[$0 + -40 >> 2]; + $16 = HEAP32[$0 + -44 >> 2]; + $6 = HEAP32[$0 + -48 >> 2]; + $10 = HEAP32[$2 >> 2]; + $43 = $10; + $44 = $10 >> 31; + $10 = HEAP32[$2 + 4 >> 2]; + $45 = $10; + $40 = $10 >> 31; + $10 = HEAP32[$2 + 8 >> 2]; + $41 = $10; + $42 = $10 >> 31; + $10 = HEAP32[$2 + 12 >> 2]; + $37 = $10; + $38 = $10 >> 31; + $10 = HEAP32[$2 + 16 >> 2]; + $39 = $10; + $34 = $10 >> 31; + $10 = HEAP32[$2 + 20 >> 2]; + $35 = $10; + $36 = $10 >> 31; + $10 = HEAP32[$2 + 24 >> 2]; + $31 = $10; + $32 = $10 >> 31; + $10 = HEAP32[$2 + 28 >> 2]; + $33 = $10; + $29 = $10 >> 31; + $10 = HEAP32[$2 + 32 >> 2]; + $30 = $10; + $26 = $10 >> 31; + $10 = HEAP32[$2 + 36 >> 2]; + $27 = $10; + $28 = $10 >> 31; + $10 = HEAP32[$2 + 40 >> 2]; + $23 = $10; + $24 = $10 >> 31; + $2 = HEAP32[$2 + 44 >> 2]; + $25 = $2; + $21 = $2 >> 31; + $2 = 0; + while (1) { + $10 = $16; + $16 = $17; + $17 = $11; + $11 = $9; + $9 = $8; + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $13; + $13 = $2 << 2; + $22 = $13 + $5 | 0; + $13 = HEAP32[$0 + $13 >> 2]; + $14 = __wasm_i64_mul($10, $10 >> 31, $23, $24); + $19 = i64toi32_i32$HIGH_BITS; + $20 = $14; + $14 = __wasm_i64_mul($6, $6 >> 31, $25, $21); + $20 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $19 | 0; + $6 = $20 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($16, $16 >> 31, $27, $28); + $19 = $14 + $20 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($17, $17 >> 31, $30, $26); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($11, $11 >> 31, $33, $29); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($9, $9 >> 31, $31, $32); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($8, $8 >> 31, $35, $36); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($12, $12 >> 31, $39, $34); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($7, $7 >> 31, $37, $38); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($18, $18 >> 31, $41, $42); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($3, $3 >> 31, $45, $40); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = __wasm_i64_mul($15, $15 >> 31, $43, $44); + $19 = $14 + $19 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = $6; + $6 = $4; + $20 = $6 & 31; + HEAP32[$22 >> 2] = $13 - (32 <= ($6 & 63) >>> 0 ? $14 >> $20 : ((1 << $20) - 1 & $14) << 32 - $20 | $19 >>> $20); + $6 = $10; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 10) { + if (($1 | 0) < 1) { + break label$1 + } + $17 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $8 = HEAP32[$0 + -28 >> 2]; + $9 = HEAP32[$0 + -32 >> 2]; + $16 = HEAP32[$0 + -36 >> 2]; + $11 = HEAP32[$2 >> 2]; + $34 = $11; + $35 = $11 >> 31; + $11 = HEAP32[$2 + 4 >> 2]; + $36 = $11; + $31 = $11 >> 31; + $11 = HEAP32[$2 + 8 >> 2]; + $32 = $11; + $33 = $11 >> 31; + $11 = HEAP32[$2 + 12 >> 2]; + $29 = $11; + $30 = $11 >> 31; + $11 = HEAP32[$2 + 16 >> 2]; + $26 = $11; + $27 = $11 >> 31; + $11 = HEAP32[$2 + 20 >> 2]; + $28 = $11; + $23 = $11 >> 31; + $11 = HEAP32[$2 + 24 >> 2]; + $24 = $11; + $25 = $11 >> 31; + $11 = HEAP32[$2 + 28 >> 2]; + $21 = $11; + $22 = $11 >> 31; + $2 = HEAP32[$2 + 32 >> 2]; + $20 = $2; + $19 = $2 >> 31; + $2 = 0; + while (1) { + $11 = $9; + $9 = $8; + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $17; + $6 = $2 << 2; + $14 = $6 + $5 | 0; + $17 = HEAP32[$0 + $6 >> 2]; + $10 = __wasm_i64_mul($11, $11 >> 31, $21, $22); + $6 = i64toi32_i32$HIGH_BITS; + $16 = __wasm_i64_mul($16, $16 >> 31, $20, $19); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($9, $9 >> 31, $24, $25); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($8, $8 >> 31, $28, $23); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($12, $12 >> 31, $26, $27); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($7, $7 >> 31, $29, $30); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($18, $18 >> 31, $32, $33); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($3, $3 >> 31, $36, $31); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = __wasm_i64_mul($15, $15 >> 31, $34, $35); + $10 = $16 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $6; + $6 = $4; + $13 = $6 & 31; + HEAP32[$14 >> 2] = $17 - (32 <= ($6 & 63) >>> 0 ? $16 >> $13 : ((1 << $13) - 1 & $16) << 32 - $13 | $10 >>> $13); + $16 = $11; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $16 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $8 = HEAP32[$0 + -28 >> 2]; + $9 = HEAP32[$0 + -32 >> 2]; + $11 = HEAP32[$0 + -36 >> 2]; + $10 = HEAP32[$0 + -40 >> 2]; + $6 = HEAP32[$2 >> 2]; + $37 = $6; + $38 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $39 = $6; + $34 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $35 = $6; + $36 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $31 = $6; + $32 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $33 = $6; + $29 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $30 = $6; + $26 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $27 = $6; + $28 = $6 >> 31; + $6 = HEAP32[$2 + 28 >> 2]; + $23 = $6; + $24 = $6 >> 31; + $6 = HEAP32[$2 + 32 >> 2]; + $25 = $6; + $21 = $6 >> 31; + $2 = HEAP32[$2 + 36 >> 2]; + $22 = $2; + $20 = $2 >> 31; + $2 = 0; + while (1) { + $17 = $11; + $11 = $9; + $9 = $8; + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $16; + $6 = $2 << 2; + $19 = $6 + $5 | 0; + $16 = HEAP32[$0 + $6 >> 2]; + $13 = __wasm_i64_mul($17, $17 >> 31, $25, $21); + $6 = i64toi32_i32$HIGH_BITS; + $10 = __wasm_i64_mul($10, $10 >> 31, $22, $20); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($11, $11 >> 31, $23, $24); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($9, $9 >> 31, $27, $28); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($8, $8 >> 31, $30, $26); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($12, $12 >> 31, $33, $29); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($7, $7 >> 31, $31, $32); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($18, $18 >> 31, $35, $36); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($3, $3 >> 31, $39, $34); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = __wasm_i64_mul($15, $15 >> 31, $37, $38); + $13 = $10 + $13 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = $6; + $6 = $4; + $14 = $6 & 31; + HEAP32[$19 >> 2] = $16 - (32 <= ($6 & 63) >>> 0 ? $10 >> $14 : ((1 << $14) - 1 & $10) << 32 - $14 | $13 >>> $14); + $10 = $17; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 5) { + if ($3 >>> 0 >= 7) { + if (($3 | 0) != 8) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $11 = HEAP32[$0 + -28 >> 2]; + $8 = HEAP32[$2 >> 2]; + $29 = $8; + $30 = $8 >> 31; + $8 = HEAP32[$2 + 4 >> 2]; + $26 = $8; + $27 = $8 >> 31; + $8 = HEAP32[$2 + 8 >> 2]; + $28 = $8; + $23 = $8 >> 31; + $8 = HEAP32[$2 + 12 >> 2]; + $24 = $8; + $25 = $8 >> 31; + $8 = HEAP32[$2 + 16 >> 2]; + $21 = $8; + $22 = $8 >> 31; + $8 = HEAP32[$2 + 20 >> 2]; + $20 = $8; + $19 = $8 >> 31; + $2 = HEAP32[$2 + 24 >> 2]; + $14 = $2; + $13 = $2 >> 31; + $2 = 0; + while (1) { + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $9; + $9 = $2 << 2; + $10 = $9 + $5 | 0; + $9 = HEAP32[$0 + $9 >> 2]; + $17 = __wasm_i64_mul($8, $8 >> 31, $20, $19); + $6 = i64toi32_i32$HIGH_BITS; + $11 = __wasm_i64_mul($11, $11 >> 31, $14, $13); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = __wasm_i64_mul($12, $12 >> 31, $21, $22); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = __wasm_i64_mul($7, $7 >> 31, $24, $25); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = __wasm_i64_mul($18, $18 >> 31, $28, $23); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = __wasm_i64_mul($3, $3 >> 31, $26, $27); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = __wasm_i64_mul($15, $15 >> 31, $29, $30); + $17 = $11 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $4 & 31; + HEAP32[$10 >> 2] = $9 - (32 <= ($4 & 63) >>> 0 ? $6 >> $16 : ((1 << $16) - 1 & $6) << 32 - $16 | $17 >>> $16); + $11 = $8; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $11 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $12 = HEAP32[$0 + -24 >> 2]; + $8 = HEAP32[$0 + -28 >> 2]; + $17 = HEAP32[$0 + -32 >> 2]; + $9 = HEAP32[$2 >> 2]; + $31 = $9; + $32 = $9 >> 31; + $9 = HEAP32[$2 + 4 >> 2]; + $33 = $9; + $29 = $9 >> 31; + $9 = HEAP32[$2 + 8 >> 2]; + $30 = $9; + $26 = $9 >> 31; + $9 = HEAP32[$2 + 12 >> 2]; + $27 = $9; + $28 = $9 >> 31; + $9 = HEAP32[$2 + 16 >> 2]; + $23 = $9; + $24 = $9 >> 31; + $9 = HEAP32[$2 + 20 >> 2]; + $25 = $9; + $21 = $9 >> 31; + $9 = HEAP32[$2 + 24 >> 2]; + $22 = $9; + $20 = $9 >> 31; + $2 = HEAP32[$2 + 28 >> 2]; + $19 = $2; + $14 = $2 >> 31; + $2 = 0; + while (1) { + $9 = $8; + $8 = $12; + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $11; + $11 = $2 << 2; + $13 = $11 + $5 | 0; + $11 = HEAP32[$0 + $11 >> 2]; + $16 = __wasm_i64_mul($9, $9 >> 31, $22, $20); + $6 = i64toi32_i32$HIGH_BITS; + $17 = __wasm_i64_mul($17, $17 >> 31, $19, $14); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($8, $8 >> 31, $25, $21); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($12, $12 >> 31, $23, $24); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($7, $7 >> 31, $27, $28); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($18, $18 >> 31, $30, $26); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($3, $3 >> 31, $33, $29); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = __wasm_i64_mul($15, $15 >> 31, $31, $32); + $16 = $17 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $6; + $6 = $4; + $10 = $6 & 31; + HEAP32[$13 >> 2] = $11 - (32 <= ($6 & 63) >>> 0 ? $17 >> $10 : ((1 << $10) - 1 & $17) << 32 - $10 | $16 >>> $10); + $17 = $9; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 6) { + if (($1 | 0) < 1) { + break label$1 + } + $12 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $8 = HEAP32[$0 + -20 >> 2]; + $7 = HEAP32[$2 >> 2]; + $23 = $7; + $24 = $7 >> 31; + $7 = HEAP32[$2 + 4 >> 2]; + $25 = $7; + $21 = $7 >> 31; + $7 = HEAP32[$2 + 8 >> 2]; + $22 = $7; + $20 = $7 >> 31; + $7 = HEAP32[$2 + 12 >> 2]; + $19 = $7; + $14 = $7 >> 31; + $2 = HEAP32[$2 + 16 >> 2]; + $13 = $2; + $10 = $2 >> 31; + $2 = 0; + while (1) { + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $12; + $12 = $2 << 2; + $16 = $12 + $5 | 0; + $12 = HEAP32[$0 + $12 >> 2]; + $11 = __wasm_i64_mul($7, $7 >> 31, $19, $14); + $9 = i64toi32_i32$HIGH_BITS; + $8 = __wasm_i64_mul($8, $8 >> 31, $13, $10); + $11 = $8 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $9 | 0; + $6 = $11 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $8 = __wasm_i64_mul($18, $18 >> 31, $22, $20); + $9 = $8 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $8 = __wasm_i64_mul($3, $3 >> 31, $25, $21); + $9 = $8 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $8 = __wasm_i64_mul($15, $15 >> 31, $23, $24); + $9 = $8 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $11 = $4 & 31; + HEAP32[$16 >> 2] = $12 - (32 <= ($4 & 63) >>> 0 ? $6 >> $11 : ((1 << $11) - 1 & $6) << 32 - $11 | $9 >>> $11); + $8 = $7; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $8 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $18 = HEAP32[$0 + -16 >> 2]; + $7 = HEAP32[$0 + -20 >> 2]; + $9 = HEAP32[$0 + -24 >> 2]; + $12 = HEAP32[$2 >> 2]; + $27 = $12; + $28 = $12 >> 31; + $12 = HEAP32[$2 + 4 >> 2]; + $23 = $12; + $24 = $12 >> 31; + $12 = HEAP32[$2 + 8 >> 2]; + $25 = $12; + $21 = $12 >> 31; + $12 = HEAP32[$2 + 12 >> 2]; + $22 = $12; + $20 = $12 >> 31; + $12 = HEAP32[$2 + 16 >> 2]; + $19 = $12; + $14 = $12 >> 31; + $2 = HEAP32[$2 + 20 >> 2]; + $13 = $2; + $10 = $2 >> 31; + $2 = 0; + while (1) { + $12 = $7; + $7 = $18; + $18 = $3; + $3 = $15; + $15 = $8; + $8 = $2 << 2; + $16 = $8 + $5 | 0; + $8 = HEAP32[$0 + $8 >> 2]; + $6 = __wasm_i64_mul($12, $12 >> 31, $19, $14); + $11 = i64toi32_i32$HIGH_BITS; + $9 = __wasm_i64_mul($9, $9 >> 31, $13, $10); + $26 = $9 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $11 | 0; + $6 = $26 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = __wasm_i64_mul($7, $7 >> 31, $22, $20); + $11 = $9 + $26 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = __wasm_i64_mul($18, $18 >> 31, $25, $21); + $11 = $9 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = __wasm_i64_mul($3, $3 >> 31, $23, $24); + $11 = $9 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = __wasm_i64_mul($15, $15 >> 31, $27, $28); + $11 = $9 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $4 & 31; + HEAP32[$16 >> 2] = $8 - (32 <= ($4 & 63) >>> 0 ? $6 >> $17 : ((1 << $17) - 1 & $6) << 32 - $17 | $11 >>> $17); + $9 = $12; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 3) { + if (($3 | 0) != 4) { + if (($1 | 0) < 1) { + break label$1 + } + $18 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $7 = HEAP32[$0 + -12 >> 2]; + $3 = HEAP32[$2 >> 2]; + $19 = $3; + $14 = $3 >> 31; + $3 = HEAP32[$2 + 4 >> 2]; + $13 = $3; + $10 = $3 >> 31; + $2 = HEAP32[$2 + 8 >> 2]; + $16 = $2; + $17 = $2 >> 31; + $2 = 0; + while (1) { + $3 = $15; + $15 = $18; + $18 = $2 << 2; + $11 = $18 + $5 | 0; + $18 = HEAP32[$0 + $18 >> 2]; + $9 = $18; + $8 = __wasm_i64_mul($3, $3 >> 31, $13, $10); + $12 = i64toi32_i32$HIGH_BITS; + $7 = __wasm_i64_mul($7, $7 >> 31, $16, $17); + $8 = $7 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $12 | 0; + $6 = $8 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; + $7 = __wasm_i64_mul($15, $15 >> 31, $19, $14); + $12 = $7 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; + $7 = $4; + $8 = $7 & 31; + HEAP32[$11 >> 2] = $9 - (32 <= ($7 & 63) >>> 0 ? $6 >> $8 : ((1 << $8) - 1 & $6) << 32 - $8 | $12 >>> $8); + $7 = $3; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $7 = HEAP32[$0 + -4 >> 2]; + $15 = HEAP32[$0 + -8 >> 2]; + $3 = HEAP32[$0 + -12 >> 2]; + $12 = HEAP32[$0 + -16 >> 2]; + $18 = HEAP32[$2 >> 2]; + $21 = $18; + $22 = $18 >> 31; + $18 = HEAP32[$2 + 4 >> 2]; + $20 = $18; + $19 = $18 >> 31; + $18 = HEAP32[$2 + 8 >> 2]; + $14 = $18; + $13 = $14 >> 31; + $2 = HEAP32[$2 + 12 >> 2]; + $10 = $2; + $16 = $2 >> 31; + $2 = 0; + while (1) { + $18 = $3; + $3 = $15; + $15 = $7; + $7 = $2 << 2; + $17 = $7 + $5 | 0; + $7 = HEAP32[$0 + $7 >> 2]; + $9 = __wasm_i64_mul($18, $18 >> 31, $14, $13); + $8 = i64toi32_i32$HIGH_BITS; + $12 = __wasm_i64_mul($12, $12 >> 31, $10, $16); + $9 = $12 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $8 | 0; + $6 = $9 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $12 = __wasm_i64_mul($3, $3 >> 31, $20, $19); + $8 = $12 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $12 = __wasm_i64_mul($15, $15 >> 31, $21, $22); + $8 = $12 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $4 & 31; + HEAP32[$17 >> 2] = $7 - (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $8 >>> $9); + $12 = $18; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 2) { + if (($1 | 0) < 1) { + break label$1 + } + $15 = HEAP32[$0 + -4 >> 2]; + $2 = HEAP32[$2 >> 2]; + $9 = $2; + $8 = $2 >> 31; + $2 = 0; + while (1) { + $3 = $2 << 2; + $6 = $3 + $5 | 0; + $18 = HEAP32[$0 + $3 >> 2]; + $15 = __wasm_i64_mul($15, $15 >> 31, $9, $8); + $7 = i64toi32_i32$HIGH_BITS; + $3 = $4; + $12 = $3 & 31; + HEAP32[$6 >> 2] = $18 - (32 <= ($3 & 63) >>> 0 ? $7 >> $12 : ((1 << $12) - 1 & $7) << 32 - $12 | $15 >>> $12); + $15 = $18; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $3 = HEAP32[$0 + -4 >> 2]; + $18 = HEAP32[$0 + -8 >> 2]; + $15 = HEAP32[$2 >> 2]; + $10 = $15; + $16 = $10 >> 31; + $2 = HEAP32[$2 + 4 >> 2]; + $17 = $2; + $11 = $2 >> 31; + $2 = 0; + while (1) { + $15 = $3; + $3 = $2 << 2; + $9 = $3 + $5 | 0; + $3 = HEAP32[$0 + $3 >> 2]; + $12 = __wasm_i64_mul($15, $15 >> 31, $10, $16); + $7 = i64toi32_i32$HIGH_BITS; + $18 = __wasm_i64_mul($18, $18 >> 31, $17, $11); + $12 = $18 + $12 | 0; + $6 = i64toi32_i32$HIGH_BITS + $7 | 0; + $6 = $12 >>> 0 < $18 >>> 0 ? $6 + 1 | 0 : $6; + $7 = $12; + $12 = $4 & 31; + HEAP32[$9 >> 2] = $3 - (32 <= ($4 & 63) >>> 0 ? $6 >> $12 : ((1 << $12) - 1 & $6) << 32 - $12 | $7 >>> $12); + $18 = $15; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__lpc_restore_signal($0, $1, $2, $3, $4, $5) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0; + label$1 : { + if ($3 >>> 0 >= 13) { + if (($1 | 0) < 1) { + break label$1 + } + $17 = $3 + -13 | 0; + while (1) { + $25 = 0; + $26 = 0; + $23 = 0; + $24 = 0; + $21 = 0; + $22 = 0; + $19 = 0; + $20 = 0; + $18 = 0; + $15 = 0; + $12 = 0; + $10 = 0; + $14 = 0; + $9 = 0; + $13 = 0; + $7 = 0; + $16 = 0; + $11 = 0; + $8 = 0; + $3 = 0; + label$4 : { + switch ($17 | 0) { + case 19: + $25 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -128 >> 2], HEAP32[$2 + 124 >> 2]); + case 18: + $26 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -124 >> 2], HEAP32[$2 + 120 >> 2]) + $25 | 0; + case 17: + $23 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -120 >> 2], HEAP32[$2 + 116 >> 2]) + $26 | 0; + case 16: + $24 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -116 >> 2], HEAP32[$2 + 112 >> 2]) + $23 | 0; + case 15: + $21 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -112 >> 2], HEAP32[$2 + 108 >> 2]) + $24 | 0; + case 14: + $22 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -108 >> 2], HEAP32[$2 + 104 >> 2]) + $21 | 0; + case 13: + $19 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -104 >> 2], HEAP32[$2 + 100 >> 2]) + $22 | 0; + case 12: + $20 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -100 >> 2], HEAP32[$2 + 96 >> 2]) + $19 | 0; + case 11: + $18 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -96 >> 2], HEAP32[$2 + 92 >> 2]) + $20 | 0; + case 10: + $15 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -92 >> 2], HEAP32[$2 + 88 >> 2]) + $18 | 0; + case 9: + $12 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -88 >> 2], HEAP32[$2 + 84 >> 2]) + $15 | 0; + case 8: + $10 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -84 >> 2], HEAP32[$2 + 80 >> 2]) + $12 | 0; + case 7: + $14 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -80 >> 2], HEAP32[$2 + 76 >> 2]) + $10 | 0; + case 6: + $9 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -76 >> 2], HEAP32[$2 + 72 >> 2]) + $14 | 0; + case 5: + $13 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -72 >> 2], HEAP32[$2 + 68 >> 2]) + $9 | 0; + case 4: + $7 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -68 >> 2], HEAP32[$2 + 64 >> 2]) + $13 | 0; + case 3: + $16 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -64 >> 2], HEAP32[$2 + 60 >> 2]) + $7 | 0; + case 2: + $11 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -60 >> 2], HEAP32[$2 + 56 >> 2]) + $16 | 0; + case 1: + $8 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -56 >> 2], HEAP32[$2 + 52 >> 2]) + $11 | 0; + case 0: + $3 = ($6 << 2) + $5 | 0; + $3 = ((((((((((((Math_imul(HEAP32[$3 + -52 >> 2], HEAP32[$2 + 48 >> 2]) + $8 | 0) + Math_imul(HEAP32[$3 + -48 >> 2], HEAP32[$2 + 44 >> 2]) | 0) + Math_imul(HEAP32[$3 + -44 >> 2], HEAP32[$2 + 40 >> 2]) | 0) + Math_imul(HEAP32[$3 + -40 >> 2], HEAP32[$2 + 36 >> 2]) | 0) + Math_imul(HEAP32[$3 + -36 >> 2], HEAP32[$2 + 32 >> 2]) | 0) + Math_imul(HEAP32[$3 + -32 >> 2], HEAP32[$2 + 28 >> 2]) | 0) + Math_imul(HEAP32[$3 + -28 >> 2], HEAP32[$2 + 24 >> 2]) | 0) + Math_imul(HEAP32[$3 + -24 >> 2], HEAP32[$2 + 20 >> 2]) | 0) + Math_imul(HEAP32[$3 + -20 >> 2], HEAP32[$2 + 16 >> 2]) | 0) + Math_imul(HEAP32[$3 + -16 >> 2], HEAP32[$2 + 12 >> 2]) | 0) + Math_imul(HEAP32[$3 + -12 >> 2], HEAP32[$2 + 8 >> 2]) | 0) + Math_imul(HEAP32[$3 + -8 >> 2], HEAP32[$2 + 4 >> 2]) | 0) + Math_imul(HEAP32[$3 + -4 >> 2], HEAP32[$2 >> 2]) | 0; + break; + default: + break label$4; + }; + } + $8 = $6 << 2; + HEAP32[$8 + $5 >> 2] = HEAP32[$0 + $8 >> 2] + ($3 >> $4); + $6 = $6 + 1 | 0; + if (($6 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 9) { + if ($3 >>> 0 >= 11) { + if (($3 | 0) != 12) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $9 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $10 = HEAP32[$5 + -40 >> 2]; + $12 = HEAP32[$5 + -44 >> 2]; + $27 = HEAP32[$2 >> 2]; + $28 = HEAP32[$2 + 4 >> 2]; + $25 = HEAP32[$2 + 8 >> 2]; + $26 = HEAP32[$2 + 12 >> 2]; + $23 = HEAP32[$2 + 16 >> 2]; + $24 = HEAP32[$2 + 20 >> 2]; + $21 = HEAP32[$2 + 24 >> 2]; + $22 = HEAP32[$2 + 28 >> 2]; + $19 = HEAP32[$2 + 32 >> 2]; + $20 = HEAP32[$2 + 36 >> 2]; + $18 = HEAP32[$2 + 40 >> 2]; + $2 = 0; + while (1) { + $17 = $10; + $12 = Math_imul($10, $20) + Math_imul($12, $18) | 0; + $10 = $14; + $12 = $12 + Math_imul($19, $10) | 0; + $14 = $9; + $12 = Math_imul($9, $22) + $12 | 0; + $9 = $13; + $12 = $12 + Math_imul($21, $9) | 0; + $13 = $7; + $12 = Math_imul($7, $24) + $12 | 0; + $7 = $16; + $12 = $12 + Math_imul($23, $7) | 0; + $16 = $11; + $12 = Math_imul($11, $26) + $12 | 0; + $11 = $8; + $15 = Math_imul($8, $25) + $12 | 0; + $8 = $3; + $12 = $2 << 2; + $15 = Math_imul($3, $28) + $15 | 0; + $3 = $6; + $6 = HEAP32[$12 + $0 >> 2] + ($15 + Math_imul($27, $3) >> $4) | 0; + HEAP32[$5 + $12 >> 2] = $6; + $12 = $17; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $9 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $10 = HEAP32[$5 + -40 >> 2]; + $12 = HEAP32[$5 + -44 >> 2]; + $15 = HEAP32[$5 + -48 >> 2]; + $29 = HEAP32[$2 >> 2]; + $30 = HEAP32[$2 + 4 >> 2]; + $27 = HEAP32[$2 + 8 >> 2]; + $28 = HEAP32[$2 + 12 >> 2]; + $25 = HEAP32[$2 + 16 >> 2]; + $26 = HEAP32[$2 + 20 >> 2]; + $23 = HEAP32[$2 + 24 >> 2]; + $24 = HEAP32[$2 + 28 >> 2]; + $21 = HEAP32[$2 + 32 >> 2]; + $22 = HEAP32[$2 + 36 >> 2]; + $19 = HEAP32[$2 + 40 >> 2]; + $20 = HEAP32[$2 + 44 >> 2]; + $2 = 0; + while (1) { + $17 = $12; + $15 = Math_imul($12, $19) + Math_imul($15, $20) | 0; + $12 = $10; + $15 = Math_imul($10, $22) + $15 | 0; + $10 = $14; + $15 = $15 + Math_imul($21, $10) | 0; + $14 = $9; + $15 = Math_imul($9, $24) + $15 | 0; + $9 = $13; + $15 = $15 + Math_imul($23, $9) | 0; + $13 = $7; + $15 = Math_imul($7, $26) + $15 | 0; + $7 = $16; + $15 = $15 + Math_imul($25, $7) | 0; + $16 = $11; + $15 = Math_imul($11, $28) + $15 | 0; + $11 = $8; + $18 = Math_imul($8, $27) + $15 | 0; + $8 = $3; + $15 = $2 << 2; + $18 = Math_imul($3, $30) + $18 | 0; + $3 = $6; + $6 = HEAP32[$15 + $0 >> 2] + ($18 + Math_imul($29, $3) >> $4) | 0; + HEAP32[$5 + $15 >> 2] = $6; + $15 = $17; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 10) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $9 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $23 = HEAP32[$2 >> 2]; + $24 = HEAP32[$2 + 4 >> 2]; + $21 = HEAP32[$2 + 8 >> 2]; + $22 = HEAP32[$2 + 12 >> 2]; + $19 = HEAP32[$2 + 16 >> 2]; + $20 = HEAP32[$2 + 20 >> 2]; + $18 = HEAP32[$2 + 24 >> 2]; + $15 = HEAP32[$2 + 28 >> 2]; + $17 = HEAP32[$2 + 32 >> 2]; + $2 = 0; + while (1) { + $10 = $9; + $14 = Math_imul($9, $15) + Math_imul($14, $17) | 0; + $9 = $13; + $14 = $14 + Math_imul($18, $9) | 0; + $13 = $7; + $14 = Math_imul($7, $20) + $14 | 0; + $7 = $16; + $14 = $14 + Math_imul($19, $7) | 0; + $16 = $11; + $14 = Math_imul($11, $22) + $14 | 0; + $11 = $8; + $12 = Math_imul($8, $21) + $14 | 0; + $8 = $3; + $14 = $2 << 2; + $12 = Math_imul($3, $24) + $12 | 0; + $3 = $6; + $6 = HEAP32[$14 + $0 >> 2] + ($12 + Math_imul($23, $3) >> $4) | 0; + HEAP32[$5 + $14 >> 2] = $6; + $14 = $10; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $9 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $10 = HEAP32[$5 + -40 >> 2]; + $25 = HEAP32[$2 >> 2]; + $26 = HEAP32[$2 + 4 >> 2]; + $23 = HEAP32[$2 + 8 >> 2]; + $24 = HEAP32[$2 + 12 >> 2]; + $21 = HEAP32[$2 + 16 >> 2]; + $22 = HEAP32[$2 + 20 >> 2]; + $19 = HEAP32[$2 + 24 >> 2]; + $20 = HEAP32[$2 + 28 >> 2]; + $18 = HEAP32[$2 + 32 >> 2]; + $15 = HEAP32[$2 + 36 >> 2]; + $2 = 0; + while (1) { + $12 = $14; + $10 = Math_imul($18, $12) + Math_imul($10, $15) | 0; + $14 = $9; + $10 = Math_imul($9, $20) + $10 | 0; + $9 = $13; + $10 = $10 + Math_imul($19, $9) | 0; + $13 = $7; + $10 = Math_imul($7, $22) + $10 | 0; + $7 = $16; + $10 = $10 + Math_imul($21, $7) | 0; + $16 = $11; + $10 = Math_imul($11, $24) + $10 | 0; + $11 = $8; + $17 = Math_imul($8, $23) + $10 | 0; + $8 = $3; + $10 = $2 << 2; + $17 = Math_imul($3, $26) + $17 | 0; + $3 = $6; + $6 = HEAP32[$10 + $0 >> 2] + ($17 + Math_imul($25, $3) >> $4) | 0; + HEAP32[$5 + $10 >> 2] = $6; + $10 = $12; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 5) { + if ($3 >>> 0 >= 7) { + if (($3 | 0) != 8) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $19 = HEAP32[$2 >> 2]; + $20 = HEAP32[$2 + 4 >> 2]; + $18 = HEAP32[$2 + 8 >> 2]; + $15 = HEAP32[$2 + 12 >> 2]; + $17 = HEAP32[$2 + 16 >> 2]; + $12 = HEAP32[$2 + 20 >> 2]; + $10 = HEAP32[$2 + 24 >> 2]; + $2 = 0; + while (1) { + $9 = $7; + $13 = Math_imul($7, $12) + Math_imul($10, $13) | 0; + $7 = $16; + $13 = $13 + Math_imul($17, $7) | 0; + $16 = $11; + $13 = Math_imul($11, $15) + $13 | 0; + $11 = $8; + $14 = Math_imul($8, $18) + $13 | 0; + $8 = $3; + $13 = $2 << 2; + $14 = Math_imul($3, $20) + $14 | 0; + $3 = $6; + $6 = HEAP32[$13 + $0 >> 2] + ($14 + Math_imul($19, $3) >> $4) | 0; + HEAP32[$5 + $13 >> 2] = $6; + $13 = $9; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $13 = HEAP32[$5 + -28 >> 2]; + $9 = HEAP32[$5 + -32 >> 2]; + $21 = HEAP32[$2 >> 2]; + $22 = HEAP32[$2 + 4 >> 2]; + $19 = HEAP32[$2 + 8 >> 2]; + $20 = HEAP32[$2 + 12 >> 2]; + $18 = HEAP32[$2 + 16 >> 2]; + $15 = HEAP32[$2 + 20 >> 2]; + $17 = HEAP32[$2 + 24 >> 2]; + $12 = HEAP32[$2 + 28 >> 2]; + $2 = 0; + while (1) { + $14 = $13; + $9 = Math_imul($17, $13) + Math_imul($9, $12) | 0; + $13 = $7; + $9 = Math_imul($7, $15) + $9 | 0; + $7 = $16; + $9 = $9 + Math_imul($18, $7) | 0; + $16 = $11; + $9 = Math_imul($11, $20) + $9 | 0; + $11 = $8; + $10 = Math_imul($8, $19) + $9 | 0; + $8 = $3; + $9 = $2 << 2; + $10 = Math_imul($3, $22) + $10 | 0; + $3 = $6; + $6 = HEAP32[$9 + $0 >> 2] + ($10 + Math_imul($21, $3) >> $4) | 0; + HEAP32[$5 + $9 >> 2] = $6; + $9 = $14; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 6) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $17 = HEAP32[$2 >> 2]; + $12 = HEAP32[$2 + 4 >> 2]; + $10 = HEAP32[$2 + 8 >> 2]; + $14 = HEAP32[$2 + 12 >> 2]; + $9 = HEAP32[$2 + 16 >> 2]; + $2 = 0; + while (1) { + $7 = $11; + $16 = Math_imul($14, $7) + Math_imul($9, $16) | 0; + $11 = $8; + $13 = Math_imul($8, $10) + $16 | 0; + $8 = $3; + $16 = $2 << 2; + $13 = Math_imul($3, $12) + $13 | 0; + $3 = $6; + $6 = HEAP32[$16 + $0 >> 2] + ($13 + Math_imul($17, $3) >> $4) | 0; + HEAP32[$5 + $16 >> 2] = $6; + $16 = $7; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $16 = HEAP32[$5 + -20 >> 2]; + $7 = HEAP32[$5 + -24 >> 2]; + $18 = HEAP32[$2 >> 2]; + $15 = HEAP32[$2 + 4 >> 2]; + $17 = HEAP32[$2 + 8 >> 2]; + $12 = HEAP32[$2 + 12 >> 2]; + $10 = HEAP32[$2 + 16 >> 2]; + $14 = HEAP32[$2 + 20 >> 2]; + $2 = 0; + while (1) { + $13 = $16; + $7 = Math_imul($10, $13) + Math_imul($7, $14) | 0; + $16 = $11; + $7 = Math_imul($11, $12) + $7 | 0; + $11 = $8; + $9 = Math_imul($8, $17) + $7 | 0; + $8 = $3; + $7 = $2 << 2; + $9 = Math_imul($3, $15) + $9 | 0; + $3 = $6; + $6 = HEAP32[$7 + $0 >> 2] + ($9 + Math_imul($18, $3) >> $4) | 0; + HEAP32[$5 + $7 >> 2] = $6; + $7 = $13; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 3) { + if (($3 | 0) != 4) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $9 = HEAP32[$2 >> 2]; + $13 = HEAP32[$2 + 4 >> 2]; + $7 = HEAP32[$2 + 8 >> 2]; + $2 = 0; + while (1) { + $11 = $3; + $16 = $2 << 2; + $8 = Math_imul($3, $13) + Math_imul($8, $7) | 0; + $3 = $6; + $6 = HEAP32[$16 + $0 >> 2] + ($8 + Math_imul($9, $3) >> $4) | 0; + HEAP32[$5 + $16 >> 2] = $6; + $8 = $11; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $8 = HEAP32[$5 + -12 >> 2]; + $11 = HEAP32[$5 + -16 >> 2]; + $10 = HEAP32[$2 >> 2]; + $14 = HEAP32[$2 + 4 >> 2]; + $9 = HEAP32[$2 + 8 >> 2]; + $13 = HEAP32[$2 + 12 >> 2]; + $2 = 0; + while (1) { + $16 = $8; + $7 = Math_imul($8, $9) + Math_imul($11, $13) | 0; + $8 = $3; + $11 = $2 << 2; + $7 = Math_imul($3, $14) + $7 | 0; + $3 = $6; + $6 = HEAP32[$11 + $0 >> 2] + ($7 + Math_imul($10, $3) >> $4) | 0; + HEAP32[$5 + $11 >> 2] = $6; + $11 = $16; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 2) { + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $8 = HEAP32[$2 >> 2]; + $2 = 0; + while (1) { + $3 = $2 << 2; + $6 = HEAP32[$3 + $0 >> 2] + (Math_imul($6, $8) >> $4) | 0; + HEAP32[$3 + $5 >> 2] = $6; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $6 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $7 = HEAP32[$2 >> 2]; + $16 = HEAP32[$2 + 4 >> 2]; + $2 = 0; + while (1) { + $8 = $6; + $11 = $2 << 2; + $6 = HEAP32[$11 + $0 >> 2] + (Math_imul($6, $7) + Math_imul($3, $16) >> $4) | 0; + HEAP32[$5 + $11 >> 2] = $6; + $3 = $8; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__lpc_restore_signal_wide($0, $1, $2, $3, $4, $5) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0, $46 = 0; + label$1 : { + if ($3 >>> 0 >= 13) { + if (($1 | 0) < 1) { + break label$1 + } + $13 = $4; + $12 = $3 + -13 | 0; + while (1) { + $4 = 0; + $3 = 0; + label$4 : { + switch ($12 | 0) { + case 19: + $3 = HEAP32[(($9 << 2) + $5 | 0) + -128 >> 2]; + $4 = $3; + $7 = $3 >> 31; + $3 = HEAP32[$2 + 124 >> 2]; + $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); + $3 = i64toi32_i32$HIGH_BITS; + case 18: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -124 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 120 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 17: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -120 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 116 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 16: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -116 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 112 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 15: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -112 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 108 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 14: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -108 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 104 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 13: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -104 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 100 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 12: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -100 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 96 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 11: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -96 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 92 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 10: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -92 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 88 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 9: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -88 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 84 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 8: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -84 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 80 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 7: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -80 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 76 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 6: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -76 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 72 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 5: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -72 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 68 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 4: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -68 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 64 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 3: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -64 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 60 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 2: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -60 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 56 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 1: + $7 = HEAP32[(($9 << 2) + $5 | 0) + -56 >> 2]; + $6 = $7; + $8 = $7 >> 31; + $7 = HEAP32[$2 + 52 >> 2]; + $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $7; + $3 = $6; + case 0: + $7 = ($9 << 2) + $5 | 0; + $8 = HEAP32[$7 + -52 >> 2]; + $6 = $8; + $10 = $8 >> 31; + $8 = HEAP32[$2 + 48 >> 2]; + $8 = __wasm_i64_mul($6, $10, $8, $8 >> 31) + $4 | 0; + $6 = $3 + i64toi32_i32$HIGH_BITS | 0; + $6 = $8 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $3 = HEAP32[$7 + -48 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 44 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $4 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -44 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 40 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -40 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 36 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -36 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 32 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -32 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 28 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -28 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 24 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -24 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 20 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -20 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 16 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -16 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 12 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -12 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 8 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -8 >> 2]; + $4 = $3; + $10 = $3 >> 31; + $3 = HEAP32[$2 + 4 >> 2]; + $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = HEAP32[$7 + -4 >> 2]; + $4 = $3; + $7 = $3 >> 31; + $3 = HEAP32[$2 >> 2]; + $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); + $3 = $8 + $4 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; + $4 = $3; + $3 = $6; + break; + default: + break label$4; + }; + } + $7 = $9 << 2; + $10 = $7 + $5 | 0; + $6 = HEAP32[$0 + $7 >> 2]; + $8 = $4; + $4 = $13; + $7 = $4 & 31; + HEAP32[$10 >> 2] = $6 + (32 <= ($4 & 63) >>> 0 ? $3 >> $7 : ((1 << $7) - 1 & $3) << 32 - $7 | $8 >>> $7); + $9 = $9 + 1 | 0; + if (($9 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 9) { + if ($3 >>> 0 >= 11) { + if (($3 | 0) != 12) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $16 = HEAP32[$5 + -40 >> 2]; + $15 = HEAP32[$5 + -44 >> 2]; + $6 = HEAP32[$2 >> 2]; + $17 = $6; + $25 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $26 = $6; + $27 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $24 = $6; + $29 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $30 = $6; + $22 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $31 = $6; + $32 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $28 = $6; + $34 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $35 = $6; + $21 = $6 >> 31; + $6 = HEAP32[$2 + 28 >> 2]; + $36 = $6; + $37 = $6 >> 31; + $6 = HEAP32[$2 + 32 >> 2]; + $33 = $6; + $39 = $6 >> 31; + $6 = HEAP32[$2 + 36 >> 2]; + $40 = $6; + $20 = $6 >> 31; + $2 = HEAP32[$2 + 40 >> 2]; + $41 = $2; + $42 = $2 >> 31; + $2 = 0; + while (1) { + $6 = $2 << 2; + $38 = $6 + $5 | 0; + $43 = HEAP32[$0 + $6 >> 2]; + $18 = $16; + $6 = __wasm_i64_mul($16, $16 >> 31, $40, $20); + $44 = i64toi32_i32$HIGH_BITS; + $16 = $14; + $19 = __wasm_i64_mul($15, $15 >> 31, $41, $42); + $15 = $19 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $44 | 0; + $6 = $15 >>> 0 < $19 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $15; + $15 = __wasm_i64_mul($14, $14 >> 31, $33, $39); + $14 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $14; + $14 = $11; + $19 = $15; + $15 = __wasm_i64_mul($11, $11 >> 31, $36, $37); + $11 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $11; + $11 = $10; + $10 = $15; + $15 = __wasm_i64_mul($11, $11 >> 31, $35, $21); + $10 = $10 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $10; + $10 = $12; + $19 = $15; + $15 = __wasm_i64_mul($12, $12 >> 31, $28, $34); + $12 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $12; + $12 = $8; + $19 = $15; + $15 = __wasm_i64_mul($8, $8 >> 31, $31, $32); + $8 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $8; + $8 = $7; + $19 = $15; + $15 = __wasm_i64_mul($7, $7 >> 31, $30, $22); + $7 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $7; + $7 = $13; + $15 = __wasm_i64_mul($7, $7 >> 31, $24, $29); + $13 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $15 = $13; + $13 = $3; + $23 = $38; + $19 = $15; + $15 = __wasm_i64_mul($3, $3 >> 31, $26, $27); + $3 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $3; + $3 = $9; + $15 = __wasm_i64_mul($3, $3 >> 31, $17, $25); + $9 = $19 + $15 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; + $38 = $9; + $9 = $4; + $15 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $15 : ((1 << $15) - 1 & $6) << 32 - $15 | $38 >>> $15) + $43 | 0; + HEAP32[$23 >> 2] = $9; + $15 = $18; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $16 = HEAP32[$5 + -40 >> 2]; + $15 = HEAP32[$5 + -44 >> 2]; + $6 = HEAP32[$5 + -48 >> 2]; + $18 = HEAP32[$2 >> 2]; + $25 = $18; + $26 = $18 >> 31; + $18 = HEAP32[$2 + 4 >> 2]; + $27 = $18; + $24 = $18 >> 31; + $18 = HEAP32[$2 + 8 >> 2]; + $29 = $18; + $30 = $18 >> 31; + $18 = HEAP32[$2 + 12 >> 2]; + $22 = $18; + $31 = $18 >> 31; + $18 = HEAP32[$2 + 16 >> 2]; + $32 = $18; + $28 = $18 >> 31; + $18 = HEAP32[$2 + 20 >> 2]; + $34 = $18; + $35 = $18 >> 31; + $18 = HEAP32[$2 + 24 >> 2]; + $21 = $18; + $36 = $18 >> 31; + $18 = HEAP32[$2 + 28 >> 2]; + $37 = $18; + $33 = $18 >> 31; + $18 = HEAP32[$2 + 32 >> 2]; + $39 = $18; + $40 = $18 >> 31; + $18 = HEAP32[$2 + 36 >> 2]; + $20 = $18; + $41 = $18 >> 31; + $18 = HEAP32[$2 + 40 >> 2]; + $42 = $18; + $38 = $18 >> 31; + $2 = HEAP32[$2 + 44 >> 2]; + $43 = $2; + $44 = $2 >> 31; + $2 = 0; + while (1) { + $18 = $2 << 2; + $19 = $18 + $5 | 0; + $46 = HEAP32[$0 + $18 >> 2]; + $18 = $15; + $17 = __wasm_i64_mul($15, $15 >> 31, $42, $38); + $23 = i64toi32_i32$HIGH_BITS; + $15 = $16; + $45 = __wasm_i64_mul($6, $6 >> 31, $43, $44); + $17 = $45 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $23 | 0; + $6 = $17 >>> 0 < $45 >>> 0 ? $6 + 1 | 0 : $6; + $23 = $17; + $17 = __wasm_i64_mul($16, $16 >> 31, $20, $41); + $16 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $16; + $16 = $14; + $23 = $17; + $17 = __wasm_i64_mul($14, $14 >> 31, $39, $40); + $14 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $14 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $14; + $14 = $11; + $23 = $17; + $17 = __wasm_i64_mul($11, $11 >> 31, $37, $33); + $11 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $11; + $11 = $10; + $10 = $17; + $17 = __wasm_i64_mul($11, $11 >> 31, $21, $36); + $10 = $10 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $10; + $10 = $12; + $23 = $17; + $17 = __wasm_i64_mul($12, $12 >> 31, $34, $35); + $12 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $12; + $12 = $8; + $23 = $17; + $17 = __wasm_i64_mul($8, $8 >> 31, $32, $28); + $8 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $8; + $8 = $7; + $23 = $17; + $17 = __wasm_i64_mul($7, $7 >> 31, $22, $31); + $7 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $23 = $7; + $7 = $13; + $17 = __wasm_i64_mul($7, $7 >> 31, $29, $30); + $13 = $23 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $17 = $13; + $13 = $3; + $23 = $19; + $19 = $17; + $17 = __wasm_i64_mul($3, $3 >> 31, $27, $24); + $3 = $19 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $3; + $3 = $9; + $17 = __wasm_i64_mul($3, $3 >> 31, $25, $26); + $9 = $19 + $17 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $9; + $9 = $4; + $17 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $17 : ((1 << $17) - 1 & $6) << 32 - $17 | $19 >>> $17) + $46 | 0; + HEAP32[$23 >> 2] = $9; + $6 = $18; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 10) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $6 = HEAP32[$2 >> 2]; + $15 = $6; + $18 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $17 = $6; + $25 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $26 = $6; + $27 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $24 = $6; + $29 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $30 = $6; + $22 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $31 = $6; + $32 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $28 = $6; + $34 = $6 >> 31; + $6 = HEAP32[$2 + 28 >> 2]; + $35 = $6; + $21 = $6 >> 31; + $2 = HEAP32[$2 + 32 >> 2]; + $36 = $2; + $37 = $2 >> 31; + $2 = 0; + while (1) { + $6 = $2 << 2; + $33 = $6 + $5 | 0; + $39 = HEAP32[$0 + $6 >> 2]; + $16 = $11; + $6 = __wasm_i64_mul($11, $11 >> 31, $35, $21); + $40 = i64toi32_i32$HIGH_BITS; + $11 = $10; + $20 = __wasm_i64_mul($14, $14 >> 31, $36, $37); + $14 = $20 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $40 | 0; + $6 = $14 >>> 0 < $20 >>> 0 ? $6 + 1 | 0 : $6; + $10 = $14; + $14 = __wasm_i64_mul($11, $11 >> 31, $28, $34); + $10 = $10 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = $10; + $10 = $12; + $20 = $14; + $14 = __wasm_i64_mul($12, $12 >> 31, $31, $32); + $12 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = $12; + $12 = $8; + $20 = $14; + $14 = __wasm_i64_mul($8, $8 >> 31, $30, $22); + $8 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = $8; + $8 = $7; + $20 = $14; + $14 = __wasm_i64_mul($7, $7 >> 31, $24, $29); + $7 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $7; + $7 = $13; + $14 = __wasm_i64_mul($7, $7 >> 31, $26, $27); + $13 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $14 = $13; + $13 = $3; + $19 = $33; + $20 = $14; + $14 = __wasm_i64_mul($3, $3 >> 31, $17, $25); + $3 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $3; + $3 = $9; + $14 = __wasm_i64_mul($3, $3 >> 31, $15, $18); + $9 = $20 + $14 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; + $33 = $9; + $9 = $4; + $14 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $14 : ((1 << $14) - 1 & $6) << 32 - $14 | $33 >>> $14) + $39 | 0; + HEAP32[$19 >> 2] = $9; + $14 = $16; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$5 + -32 >> 2]; + $14 = HEAP32[$5 + -36 >> 2]; + $16 = HEAP32[$5 + -40 >> 2]; + $6 = HEAP32[$2 >> 2]; + $18 = $6; + $17 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $25 = $6; + $26 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $27 = $6; + $24 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $29 = $6; + $30 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $22 = $6; + $31 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $32 = $6; + $28 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $34 = $6; + $35 = $6 >> 31; + $6 = HEAP32[$2 + 28 >> 2]; + $21 = $6; + $36 = $6 >> 31; + $6 = HEAP32[$2 + 32 >> 2]; + $37 = $6; + $33 = $6 >> 31; + $2 = HEAP32[$2 + 36 >> 2]; + $39 = $2; + $40 = $2 >> 31; + $2 = 0; + while (1) { + $6 = $2 << 2; + $20 = $6 + $5 | 0; + $41 = HEAP32[$0 + $6 >> 2]; + $15 = $14; + $6 = __wasm_i64_mul($14, $14 >> 31, $37, $33); + $42 = i64toi32_i32$HIGH_BITS; + $14 = $11; + $38 = __wasm_i64_mul($16, $16 >> 31, $39, $40); + $16 = $38 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $42 | 0; + $6 = $16 >>> 0 < $38 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $16; + $16 = __wasm_i64_mul($11, $11 >> 31, $21, $36); + $11 = $19 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $11 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $11; + $11 = $10; + $10 = $16; + $16 = __wasm_i64_mul($11, $11 >> 31, $34, $35); + $10 = $10 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $10; + $10 = $12; + $19 = $16; + $16 = __wasm_i64_mul($12, $12 >> 31, $32, $28); + $12 = $19 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $12; + $12 = $8; + $19 = $16; + $16 = __wasm_i64_mul($8, $8 >> 31, $22, $31); + $8 = $19 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $8; + $8 = $7; + $19 = $16; + $16 = __wasm_i64_mul($7, $7 >> 31, $29, $30); + $7 = $19 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $19 = $7; + $7 = $13; + $16 = __wasm_i64_mul($7, $7 >> 31, $27, $24); + $13 = $19 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $16 = $13; + $13 = $3; + $19 = $20; + $20 = $16; + $16 = __wasm_i64_mul($3, $3 >> 31, $25, $26); + $3 = $20 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $3; + $3 = $9; + $16 = __wasm_i64_mul($3, $3 >> 31, $18, $17); + $9 = $20 + $16 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $9; + $9 = $4; + $16 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $16 : ((1 << $16) - 1 & $6) << 32 - $16 | $20 >>> $16) + $41 | 0; + HEAP32[$19 >> 2] = $9; + $16 = $15; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 5) { + if ($3 >>> 0 >= 7) { + if (($3 | 0) != 8) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$2 >> 2]; + $14 = $11; + $16 = $11 >> 31; + $11 = HEAP32[$2 + 4 >> 2]; + $15 = $11; + $18 = $11 >> 31; + $11 = HEAP32[$2 + 8 >> 2]; + $17 = $11; + $25 = $11 >> 31; + $11 = HEAP32[$2 + 12 >> 2]; + $26 = $11; + $27 = $11 >> 31; + $11 = HEAP32[$2 + 16 >> 2]; + $24 = $11; + $29 = $11 >> 31; + $11 = HEAP32[$2 + 20 >> 2]; + $30 = $11; + $22 = $11 >> 31; + $2 = HEAP32[$2 + 24 >> 2]; + $31 = $2; + $32 = $2 >> 31; + $2 = 0; + while (1) { + $11 = $2 << 2; + $28 = $11 + $5 | 0; + $34 = HEAP32[$0 + $11 >> 2]; + $11 = $12; + $6 = __wasm_i64_mul($11, $11 >> 31, $30, $22); + $35 = i64toi32_i32$HIGH_BITS; + $12 = $8; + $21 = __wasm_i64_mul($10, $10 >> 31, $31, $32); + $10 = $21 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $35 | 0; + $6 = $10 >>> 0 < $21 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $10; + $10 = __wasm_i64_mul($8, $8 >> 31, $24, $29); + $8 = $21 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = $8; + $8 = $7; + $21 = $10; + $10 = __wasm_i64_mul($7, $7 >> 31, $26, $27); + $7 = $21 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $7; + $7 = $13; + $10 = __wasm_i64_mul($7, $7 >> 31, $17, $25); + $13 = $21 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $10 = $13; + $13 = $3; + $20 = $28; + $21 = $10; + $10 = __wasm_i64_mul($3, $3 >> 31, $15, $18); + $3 = $21 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $3; + $3 = $9; + $10 = __wasm_i64_mul($3, $3 >> 31, $14, $16); + $9 = $21 + $10 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; + $28 = $9; + $9 = $4; + $10 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $10 : ((1 << $10) - 1 & $6) << 32 - $10 | $28 >>> $10) + $34 | 0; + HEAP32[$20 >> 2] = $9; + $10 = $11; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$5 + -28 >> 2]; + $11 = HEAP32[$5 + -32 >> 2]; + $6 = HEAP32[$2 >> 2]; + $16 = $6; + $15 = $6 >> 31; + $6 = HEAP32[$2 + 4 >> 2]; + $18 = $6; + $17 = $6 >> 31; + $6 = HEAP32[$2 + 8 >> 2]; + $25 = $6; + $26 = $6 >> 31; + $6 = HEAP32[$2 + 12 >> 2]; + $27 = $6; + $24 = $6 >> 31; + $6 = HEAP32[$2 + 16 >> 2]; + $29 = $6; + $30 = $6 >> 31; + $6 = HEAP32[$2 + 20 >> 2]; + $22 = $6; + $31 = $6 >> 31; + $6 = HEAP32[$2 + 24 >> 2]; + $32 = $6; + $28 = $6 >> 31; + $2 = HEAP32[$2 + 28 >> 2]; + $34 = $2; + $35 = $2 >> 31; + $2 = 0; + while (1) { + $6 = $2 << 2; + $21 = $6 + $5 | 0; + $36 = HEAP32[$0 + $6 >> 2]; + $14 = $10; + $6 = __wasm_i64_mul($10, $10 >> 31, $32, $28); + $37 = i64toi32_i32$HIGH_BITS; + $10 = $12; + $33 = __wasm_i64_mul($11, $11 >> 31, $34, $35); + $11 = $33 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $37 | 0; + $6 = $11 >>> 0 < $33 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $11; + $11 = __wasm_i64_mul($12, $12 >> 31, $22, $31); + $12 = $20 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = $12; + $12 = $8; + $20 = $11; + $11 = __wasm_i64_mul($8, $8 >> 31, $29, $30); + $8 = $20 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = $8; + $8 = $7; + $20 = $11; + $11 = __wasm_i64_mul($7, $7 >> 31, $27, $24); + $7 = $20 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $20 = $7; + $7 = $13; + $11 = __wasm_i64_mul($7, $7 >> 31, $25, $26); + $13 = $20 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $11 = $13; + $13 = $3; + $20 = $21; + $21 = $11; + $11 = __wasm_i64_mul($3, $3 >> 31, $18, $17); + $3 = $21 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $3; + $3 = $9; + $11 = __wasm_i64_mul($3, $3 >> 31, $16, $15); + $9 = $21 + $11 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $9 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $9; + $9 = $4; + $11 = $9 & 31; + $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $11 : ((1 << $11) - 1 & $6) << 32 - $11 | $21 >>> $11) + $36 | 0; + HEAP32[$20 >> 2] = $9; + $11 = $14; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 6) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$2 >> 2]; + $10 = $12; + $11 = $12 >> 31; + $12 = HEAP32[$2 + 4 >> 2]; + $14 = $12; + $16 = $12 >> 31; + $12 = HEAP32[$2 + 8 >> 2]; + $15 = $12; + $18 = $12 >> 31; + $12 = HEAP32[$2 + 12 >> 2]; + $17 = $12; + $25 = $12 >> 31; + $2 = HEAP32[$2 + 16 >> 2]; + $26 = $2; + $27 = $2 >> 31; + $2 = 0; + while (1) { + $12 = $2 << 2; + $24 = $12 + $5 | 0; + $29 = HEAP32[$0 + $12 >> 2]; + $12 = $7; + $6 = __wasm_i64_mul($7, $7 >> 31, $17, $25); + $30 = i64toi32_i32$HIGH_BITS; + $7 = $13; + $22 = __wasm_i64_mul($8, $8 >> 31, $26, $27); + $8 = $22 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $30 | 0; + $6 = $8 >>> 0 < $22 >>> 0 ? $6 + 1 | 0 : $6; + $13 = $8; + $8 = __wasm_i64_mul($7, $7 >> 31, $15, $18); + $13 = $13 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $13; + $13 = $3; + $22 = $8; + $8 = __wasm_i64_mul($3, $3 >> 31, $14, $16); + $3 = $22 + $8 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = $9; + $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); + $8 = $8 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $8 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $4 & 31; + $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $8 >>> $9) + $29 | 0; + HEAP32[$24 >> 2] = $9; + $8 = $12; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$5 + -20 >> 2]; + $12 = HEAP32[$5 + -24 >> 2]; + $10 = HEAP32[$2 >> 2]; + $11 = $10; + $14 = $11 >> 31; + $10 = HEAP32[$2 + 4 >> 2]; + $16 = $10; + $15 = $10 >> 31; + $10 = HEAP32[$2 + 8 >> 2]; + $18 = $10; + $17 = $10 >> 31; + $10 = HEAP32[$2 + 12 >> 2]; + $25 = $10; + $26 = $10 >> 31; + $10 = HEAP32[$2 + 16 >> 2]; + $27 = $10; + $24 = $10 >> 31; + $2 = HEAP32[$2 + 20 >> 2]; + $29 = $2; + $30 = $2 >> 31; + $2 = 0; + while (1) { + $10 = $2 << 2; + $22 = $10 + $5 | 0; + $31 = HEAP32[$0 + $10 >> 2]; + $10 = $8; + $6 = __wasm_i64_mul($8, $8 >> 31, $27, $24); + $32 = i64toi32_i32$HIGH_BITS; + $8 = $7; + $28 = __wasm_i64_mul($12, $12 >> 31, $29, $30); + $12 = $28 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $32 | 0; + $6 = $12 >>> 0 < $28 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $12; + $12 = __wasm_i64_mul($7, $7 >> 31, $25, $26); + $7 = $21 + $12 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $21 = $7; + $7 = $13; + $12 = __wasm_i64_mul($7, $7 >> 31, $18, $17); + $13 = $21 + $12 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $12 = $13; + $13 = $3; + $21 = $22; + $22 = $12; + $12 = __wasm_i64_mul($3, $3 >> 31, $16, $15); + $3 = $22 + $12 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; + $12 = $3; + $3 = $9; + $9 = __wasm_i64_mul($3, $3 >> 31, $11, $14); + $12 = $12 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $12 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $4 & 31; + $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $12 >>> $9) + $31 | 0; + HEAP32[$21 >> 2] = $9; + $12 = $10; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if ($3 >>> 0 >= 3) { + if (($3 | 0) != 4) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$2 >> 2]; + $12 = $7; + $10 = $7 >> 31; + $7 = HEAP32[$2 + 4 >> 2]; + $11 = $7; + $14 = $7 >> 31; + $2 = HEAP32[$2 + 8 >> 2]; + $16 = $2; + $15 = $2 >> 31; + $2 = 0; + while (1) { + $7 = $2 << 2; + $8 = $7 + $5 | 0; + $18 = HEAP32[$0 + $7 >> 2]; + $7 = $3; + $3 = __wasm_i64_mul($7, $7 >> 31, $11, $14); + $6 = i64toi32_i32$HIGH_BITS; + $17 = $8; + $13 = __wasm_i64_mul($13, $13 >> 31, $16, $15); + $3 = $13 + $3 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; + $8 = $3; + $3 = $9; + $9 = __wasm_i64_mul($3, $3 >> 31, $12, $10); + $13 = $8 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $13 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $13; + $8 = $4 & 31; + $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $8 : ((1 << $8) - 1 & $6) << 32 - $8 | $9 >>> $8) + $18 | 0; + HEAP32[$17 >> 2] = $9; + $13 = $7; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$5 + -12 >> 2]; + $7 = HEAP32[$5 + -16 >> 2]; + $8 = HEAP32[$2 >> 2]; + $10 = $8; + $11 = $8 >> 31; + $8 = HEAP32[$2 + 4 >> 2]; + $14 = $8; + $16 = $8 >> 31; + $8 = HEAP32[$2 + 8 >> 2]; + $15 = $8; + $18 = $8 >> 31; + $2 = HEAP32[$2 + 12 >> 2]; + $17 = $2; + $25 = $2 >> 31; + $2 = 0; + while (1) { + $8 = $2 << 2; + $12 = $8 + $5 | 0; + $26 = HEAP32[$0 + $8 >> 2]; + $8 = $13; + $6 = __wasm_i64_mul($8, $8 >> 31, $15, $18); + $27 = i64toi32_i32$HIGH_BITS; + $13 = $3; + $22 = $12; + $24 = __wasm_i64_mul($7, $7 >> 31, $17, $25); + $7 = $24 + $6 | 0; + $6 = i64toi32_i32$HIGH_BITS + $27 | 0; + $6 = $7 >>> 0 < $24 >>> 0 ? $6 + 1 | 0 : $6; + $12 = $7; + $7 = __wasm_i64_mul($3, $3 >> 31, $14, $16); + $3 = $12 + $7 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; + $7 = $3; + $3 = $9; + $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); + $7 = $7 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $7 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $7; + $7 = $4; + $12 = $7 & 31; + $9 = (32 <= ($7 & 63) >>> 0 ? $6 >> $12 : ((1 << $12) - 1 & $6) << 32 - $12 | $9 >>> $12) + $26 | 0; + HEAP32[$22 >> 2] = $9; + $7 = $8; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($3 | 0) != 2) { + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $2 = HEAP32[$2 >> 2]; + $8 = $2; + $12 = $2 >> 31; + $2 = 0; + while (1) { + $3 = $2 << 2; + $10 = $3 + $5 | 0; + $6 = HEAP32[$0 + $3 >> 2]; + $9 = __wasm_i64_mul($9, $9 >> 31, $8, $12); + $7 = i64toi32_i32$HIGH_BITS; + $3 = $4; + $13 = $3 & 31; + $9 = $6 + (32 <= ($3 & 63) >>> 0 ? $7 >> $13 : ((1 << $13) - 1 & $7) << 32 - $13 | $9 >>> $13) | 0; + HEAP32[$10 >> 2] = $9; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (($1 | 0) < 1) { + break label$1 + } + $9 = HEAP32[$5 + -4 >> 2]; + $3 = HEAP32[$5 + -8 >> 2]; + $13 = HEAP32[$2 >> 2]; + $8 = $13; + $12 = $8 >> 31; + $2 = HEAP32[$2 + 4 >> 2]; + $10 = $2; + $11 = $2 >> 31; + $2 = 0; + while (1) { + $13 = $2 << 2; + $7 = $13 + $5 | 0; + $14 = HEAP32[$0 + $13 >> 2]; + $13 = $9; + $9 = __wasm_i64_mul($9, $9 >> 31, $8, $12); + $6 = i64toi32_i32$HIGH_BITS; + $15 = $7; + $7 = $9; + $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); + $3 = $7 + $9 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $6 = $3 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; + $9 = $3; + $3 = $4; + $7 = $3 & 31; + $9 = (32 <= ($3 & 63) >>> 0 ? $6 >> $7 : ((1 << $7) - 1 & $6) << 32 - $7 | $9 >>> $7) + $14 | 0; + HEAP32[$15 >> 2] = $9; + $3 = $13; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__lpc_compute_expected_bits_per_residual_sample($0, $1) { + if (!!($0 > 0.0)) { + $0 = log(.5 / +($1 >>> 0) * $0) * .5 / .6931471805599453; + return $0 >= 0.0 ? $0 : 0.0; + } + return $0 < 0.0 ? 1.e+32 : 0.0; + } + + function FLAC__lpc_compute_best_order($0, $1, $2, $3) { + var $4 = 0.0, $5 = 0, $6 = 0, $7 = 0.0, $8 = 0, $9 = 0, $10 = 0.0; + $5 = 1; + if ($1) { + $10 = .5 / +($2 >>> 0); + $7 = 4294967295.0; + while (1) { + $4 = HEAPF64[($6 << 3) + $0 >> 3]; + label$3 : { + if (!!($4 > 0.0)) { + $4 = log($10 * $4) * .5 / .6931471805599453; + $4 = $4 >= 0.0 ? $4 : 0.0; + break label$3; + } + $4 = $4 < 0.0 ? 1.e+32 : 0.0; + } + $4 = $4 * +($2 - $5 >>> 0) + +(Math_imul($3, $5) >>> 0); + $8 = $4 < $7; + $7 = $8 ? $4 : $7; + $9 = $8 ? $6 : $9; + $5 = $5 + 1 | 0; + $6 = $6 + 1 | 0; + if (($6 | 0) != ($1 | 0)) { + continue + } + break; + }; + $0 = $9 + 1 | 0; + } else { + $0 = 1 + } + return $0; + } + + function strlen($0) { + var $1 = 0, $2 = 0, $3 = 0; + label$1 : { + label$2 : { + $1 = $0; + if (!($1 & 3)) { + break label$2 + } + if (!HEAPU8[$0 | 0]) { + return 0 + } + while (1) { + $1 = $1 + 1 | 0; + if (!($1 & 3)) { + break label$2 + } + if (HEAPU8[$1 | 0]) { + continue + } + break; + }; + break label$1; + } + while (1) { + $2 = $1; + $1 = $1 + 4 | 0; + $3 = HEAP32[$2 >> 2]; + if (!(($3 ^ -1) & $3 + -16843009 & -2139062144)) { + continue + } + break; + }; + if (!($3 & 255)) { + return $2 - $0 | 0 + } + while (1) { + $3 = HEAPU8[$2 + 1 | 0]; + $1 = $2 + 1 | 0; + $2 = $1; + if ($3) { + continue + } + break; + }; + } + return $1 - $0 | 0; + } + + function __strchrnul($0, $1) { + var $2 = 0, $3 = 0; + label$1 : { + $3 = $1 & 255; + if ($3) { + if ($0 & 3) { + while (1) { + $2 = HEAPU8[$0 | 0]; + if (!$2 | ($2 | 0) == ($1 & 255)) { + break label$1 + } + $0 = $0 + 1 | 0; + if ($0 & 3) { + continue + } + break; + } + } + $2 = HEAP32[$0 >> 2]; + label$5 : { + if (($2 ^ -1) & $2 + -16843009 & -2139062144) { + break label$5 + } + $3 = Math_imul($3, 16843009); + while (1) { + $2 = $2 ^ $3; + if (($2 ^ -1) & $2 + -16843009 & -2139062144) { + break label$5 + } + $2 = HEAP32[$0 + 4 >> 2]; + $0 = $0 + 4 | 0; + if (!($2 + -16843009 & ($2 ^ -1) & -2139062144)) { + continue + } + break; + }; + } + while (1) { + $2 = $0; + $3 = HEAPU8[$2 | 0]; + if ($3) { + $0 = $2 + 1 | 0; + if (($3 | 0) != ($1 & 255)) { + continue + } + } + break; + }; + return $2; + } + return strlen($0) + $0 | 0; + } + return $0; + } + + function strchr($0, $1) { + $0 = __strchrnul($0, $1); + return HEAPU8[$0 | 0] == ($1 & 255) ? $0 : 0; + } + + function __stdio_write($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $3 = global$0 - 32 | 0; + global$0 = $3; + $4 = HEAP32[$0 + 28 >> 2]; + HEAP32[$3 + 16 >> 2] = $4; + $5 = HEAP32[$0 + 20 >> 2]; + HEAP32[$3 + 28 >> 2] = $2; + HEAP32[$3 + 24 >> 2] = $1; + $1 = $5 - $4 | 0; + HEAP32[$3 + 20 >> 2] = $1; + $4 = $1 + $2 | 0; + $9 = 2; + $1 = $3 + 16 | 0; + label$1 : { + label$2 : { + label$3 : { + if (!__wasi_syscall_ret(__wasi_fd_write(HEAP32[$0 + 60 >> 2], $3 + 16 | 0, 2, $3 + 12 | 0) | 0)) { + while (1) { + $5 = HEAP32[$3 + 12 >> 2]; + if (($5 | 0) == ($4 | 0)) { + break label$3 + } + if (($5 | 0) <= -1) { + break label$2 + } + $6 = HEAP32[$1 + 4 >> 2]; + $7 = $5 >>> 0 > $6 >>> 0; + $8 = ($7 << 3) + $1 | 0; + $6 = $5 - ($7 ? $6 : 0) | 0; + HEAP32[$8 >> 2] = $6 + HEAP32[$8 >> 2]; + $8 = ($7 ? 12 : 4) + $1 | 0; + HEAP32[$8 >> 2] = HEAP32[$8 >> 2] - $6; + $4 = $4 - $5 | 0; + $1 = $7 ? $1 + 8 | 0 : $1; + $9 = $9 - $7 | 0; + if (!__wasi_syscall_ret(__wasi_fd_write(HEAP32[$0 + 60 >> 2], $1 | 0, $9 | 0, $3 + 12 | 0) | 0)) { + continue + } + break; + } + } + HEAP32[$3 + 12 >> 2] = -1; + if (($4 | 0) != -1) { + break label$2 + } + } + $1 = HEAP32[$0 + 44 >> 2]; + HEAP32[$0 + 28 >> 2] = $1; + HEAP32[$0 + 20 >> 2] = $1; + HEAP32[$0 + 16 >> 2] = $1 + HEAP32[$0 + 48 >> 2]; + $0 = $2; + break label$1; + } + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 >> 2] = HEAP32[$0 >> 2] | 32; + $0 = 0; + if (($9 | 0) == 2) { + break label$1 + } + $0 = $2 - HEAP32[$1 + 4 >> 2] | 0; + } + global$0 = $3 + 32 | 0; + return $0 | 0; + } + + function FLAC__memory_alloc_aligned_int32_array($0, $1, $2) { + var $3 = 0; + label$1 : { + if ($0 >>> 0 > 1073741823) { + break label$1 + } + $0 = dlmalloc($0 ? $0 << 2 : 1); + if (!$0) { + break label$1 + } + $3 = HEAP32[$1 >> 2]; + if ($3) { + dlfree($3) + } + HEAP32[$1 >> 2] = $0; + HEAP32[$2 >> 2] = $0; + $3 = 1; + } + return $3; + } + + function FLAC__memory_alloc_aligned_uint64_array($0, $1, $2) { + var $3 = 0; + label$1 : { + if ($0 >>> 0 > 536870911) { + break label$1 + } + $0 = dlmalloc($0 ? $0 << 3 : 1); + if (!$0) { + break label$1 + } + $3 = HEAP32[$1 >> 2]; + if ($3) { + dlfree($3) + } + HEAP32[$1 >> 2] = $0; + HEAP32[$2 >> 2] = $0; + $3 = 1; + } + return $3; + } + + function safe_malloc_mul_2op_p($0, $1) { + if (!($1 ? $0 : 0)) { + return dlmalloc(1) + } + __wasm_i64_mul($1, 0, $0, 0); + if (i64toi32_i32$HIGH_BITS) { + $0 = 0 + } else { + $0 = dlmalloc(Math_imul($0, $1)) + } + return $0; + } + + function FLAC__fixed_compute_best_predictor($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = Math_fround(0), $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if ($1) { + $3 = HEAP32[$0 + -4 >> 2]; + $8 = HEAP32[$0 + -8 >> 2]; + $12 = $3 - $8 | 0; + $5 = HEAP32[$0 + -12 >> 2]; + $9 = $12 + ($5 - $8 | 0) | 0; + $17 = $9 + ((($5 << 1) - $8 | 0) - HEAP32[$0 + -16 >> 2] | 0) | 0; + while (1) { + $8 = HEAP32[($15 << 2) + $0 >> 2]; + $5 = $8 >> 31; + $14 = ($5 ^ $5 + $8) + $14 | 0; + $5 = $8 - $3 | 0; + $11 = $5 >> 31; + $13 = ($11 ^ $5 + $11) + $13 | 0; + $11 = $5 - $12 | 0; + $3 = $11 >> 31; + $10 = ($3 ^ $3 + $11) + $10 | 0; + $9 = $11 - $9 | 0; + $3 = $9 >> 31; + $6 = ($3 ^ $3 + $9) + $6 | 0; + $12 = $9 - $17 | 0; + $3 = $12 >> 31; + $7 = ($3 ^ $3 + $12) + $7 | 0; + $3 = $8; + $12 = $5; + $17 = $9; + $9 = $11; + $15 = $15 + 1 | 0; + if (($15 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + $0 = $13 >>> 0 < $10 >>> 0 ? $13 : $10; + $0 = $0 >>> 0 < $6 >>> 0 ? $0 : $6; + label$3 : { + if ($14 >>> 0 < ($0 >>> 0 < $7 >>> 0 ? $0 : $7) >>> 0) { + break label$3 + } + $16 = 1; + $0 = $10 >>> 0 < $6 >>> 0 ? $10 : $6; + if ($13 >>> 0 < ($0 >>> 0 < $7 >>> 0 ? $0 : $7) >>> 0) { + break label$3 + } + $0 = $6 >>> 0 < $7 >>> 0; + $16 = $10 >>> 0 < ($0 ? $6 : $7) >>> 0 ? 2 : $0 ? 3 : 4; + } + $0 = $2; + if ($14) { + $4 = Math_fround(log(+($14 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $4 = Math_fround(0.0) + } + HEAPF32[$0 >> 2] = $4; + $0 = $2; + if ($13) { + $4 = Math_fround(log(+($13 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $4 = Math_fround(0.0) + } + HEAPF32[$0 + 4 >> 2] = $4; + $0 = $2; + if ($10) { + $4 = Math_fround(log(+($10 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $4 = Math_fround(0.0) + } + HEAPF32[$0 + 8 >> 2] = $4; + $0 = $2; + if ($6) { + $4 = Math_fround(log(+($6 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $4 = Math_fround(0.0) + } + HEAPF32[$0 + 12 >> 2] = $4; + if (!$7) { + HEAPF32[$2 + 16 >> 2] = 0; + return $16 | 0; + } + (wasm2js_i32$0 = $2, wasm2js_f32$0 = Math_fround(log(+($7 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453)), HEAPF32[wasm2js_i32$0 + 16 >> 2] = wasm2js_f32$0; + return $16 | 0; + } + + function FLAC__fixed_compute_best_predictor_wide($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = Math_fround(0), $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + label$1 : { + if (!$1) { + break label$1 + } + $5 = HEAP32[$0 + -4 >> 2]; + $8 = HEAP32[$0 + -8 >> 2]; + $6 = $5 - $8 | 0; + $9 = HEAP32[$0 + -12 >> 2]; + $14 = $6 + ($9 - $8 | 0) | 0; + $21 = $14 + ((($9 << 1) - $8 | 0) - HEAP32[$0 + -16 >> 2] | 0) | 0; + $9 = 0; + $8 = 0; + while (1) { + $3 = HEAP32[($20 << 2) + $0 >> 2]; + $4 = $3 >> 31; + $4 = $4 ^ $3 + $4; + $7 = $4 + $19 | 0; + if ($7 >>> 0 < $4 >>> 0) { + $18 = $18 + 1 | 0 + } + $19 = $7; + $4 = $3 - $5 | 0; + $7 = $4 >> 31; + $7 = $7 ^ $4 + $7; + $5 = $7 + $17 | 0; + if ($5 >>> 0 < $7 >>> 0) { + $15 = $15 + 1 | 0 + } + $17 = $5; + $7 = $4 - $6 | 0; + $5 = $7 >> 31; + $5 = $5 ^ $5 + $7; + $6 = $5 + $16 | 0; + if ($6 >>> 0 < $5 >>> 0) { + $10 = $10 + 1 | 0 + } + $16 = $6; + $14 = $7 - $14 | 0; + $5 = $14 >> 31; + $5 = $5 ^ $5 + $14; + $6 = $5 + $12 | 0; + if ($6 >>> 0 < $5 >>> 0) { + $8 = $8 + 1 | 0 + } + $12 = $6; + $6 = $14 - $21 | 0; + $5 = $6 >> 31; + $5 = $5 ^ $5 + $6; + $6 = $5 + $13 | 0; + if ($6 >>> 0 < $5 >>> 0) { + $9 = $9 + 1 | 0 + } + $13 = $6; + $5 = $3; + $6 = $4; + $21 = $14; + $14 = $7; + $20 = $20 + 1 | 0; + if (($20 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + $3 = ($10 | 0) == ($15 | 0) & $17 >>> 0 < $16 >>> 0 | $15 >>> 0 < $10 >>> 0; + $4 = $3 ? $17 : $16; + $0 = $4; + $3 = $3 ? $15 : $10; + $4 = ($8 | 0) == ($3 | 0) & $4 >>> 0 < $12 >>> 0 | $3 >>> 0 < $8 >>> 0; + $7 = $4 ? $0 : $12; + $3 = $4 ? $3 : $8; + $4 = ($9 | 0) == ($3 | 0) & $7 >>> 0 < $13 >>> 0 | $3 >>> 0 < $9 >>> 0; + $7 = $4 ? $7 : $13; + $3 = $4 ? $3 : $9; + $0 = 0; + label$4 : { + if (($3 | 0) == ($18 | 0) & $19 >>> 0 < $7 >>> 0 | $18 >>> 0 < $3 >>> 0) { + break label$4 + } + $3 = ($8 | 0) == ($10 | 0) & $16 >>> 0 < $12 >>> 0 | $10 >>> 0 < $8 >>> 0; + $4 = $3 ? $16 : $12; + $0 = $4; + $3 = $3 ? $10 : $8; + $4 = ($9 | 0) == ($3 | 0) & $4 >>> 0 < $13 >>> 0 | $3 >>> 0 < $9 >>> 0; + $7 = $4 ? $0 : $13; + $3 = $4 ? $3 : $9; + $0 = 1; + if (($3 | 0) == ($15 | 0) & $17 >>> 0 < $7 >>> 0 | $15 >>> 0 < $3 >>> 0) { + break label$4 + } + $0 = ($8 | 0) == ($9 | 0) & $12 >>> 0 < $13 >>> 0 | $8 >>> 0 < $9 >>> 0; + $3 = $0; + $4 = $3 ? $12 : $13; + $0 = $3 ? $8 : $9; + $0 = ($0 | 0) == ($10 | 0) & $16 >>> 0 < $4 >>> 0 | $10 >>> 0 < $0 >>> 0 ? 2 : $3 ? 3 : 4; + } + $6 = $2; + if ($18 | $19) { + $11 = Math_fround(log((+($19 >>> 0) + 4294967296.0 * +($18 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $11 = Math_fround(0.0) + } + HEAPF32[$6 >> 2] = $11; + $6 = $2; + if ($15 | $17) { + $11 = Math_fround(log((+($17 >>> 0) + 4294967296.0 * +($15 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $11 = Math_fround(0.0) + } + HEAPF32[$6 + 4 >> 2] = $11; + $6 = $2; + if ($10 | $16) { + $11 = Math_fround(log((+($16 >>> 0) + 4294967296.0 * +($10 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $11 = Math_fround(0.0) + } + HEAPF32[$6 + 8 >> 2] = $11; + $6 = $2; + if ($8 | $12) { + $11 = Math_fround(log((+($12 >>> 0) + 4294967296.0 * +($8 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) + } else { + $11 = Math_fround(0.0) + } + HEAPF32[$6 + 12 >> 2] = $11; + if (!($9 | $13)) { + HEAPF32[$2 + 16 >> 2] = 0; + return $0 | 0; + } + (wasm2js_i32$0 = $2, wasm2js_f32$0 = Math_fround(log((+($13 >>> 0) + 4294967296.0 * +($9 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453)), HEAPF32[wasm2js_i32$0 + 16 >> 2] = wasm2js_f32$0; + return $0 | 0; + } + + function FLAC__fixed_compute_residual($0, $1, $2, $3) { + var $4 = 0, $5 = 0; + label$1 : { + label$2 : { + label$3 : { + switch ($2 | 0) { + case 4: + $2 = 0; + if (($1 | 0) <= 0) { + break label$2 + } + while (1) { + $5 = $2 << 2; + $4 = $5 + $0 | 0; + HEAP32[$3 + $5 >> 2] = (HEAP32[$4 + -16 >> 2] + (HEAP32[$4 >> 2] + Math_imul(HEAP32[$4 + -8 >> 2], 6) | 0) | 0) - (HEAP32[$4 + -12 >> 2] + HEAP32[$4 + -4 >> 2] << 2); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 3: + $2 = 0; + if (($1 | 0) <= 0) { + break label$2 + } + while (1) { + $5 = $2 << 2; + $4 = $5 + $0 | 0; + HEAP32[$3 + $5 >> 2] = (HEAP32[$4 >> 2] - HEAP32[$4 + -12 >> 2] | 0) + Math_imul(HEAP32[$4 + -8 >> 2] - HEAP32[$4 + -4 >> 2] | 0, 3); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 2: + $2 = 0; + if (($1 | 0) <= 0) { + break label$2 + } + while (1) { + $5 = $2 << 2; + $4 = $5 + $0 | 0; + HEAP32[$3 + $5 >> 2] = HEAP32[$4 + -8 >> 2] + (HEAP32[$4 >> 2] - (HEAP32[$4 + -4 >> 2] << 1) | 0); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 0: + break label$1; + case 1: + break label$3; + default: + break label$2; + }; + } + $2 = 0; + if (($1 | 0) <= 0) { + break label$2 + } + while (1) { + $5 = $2 << 2; + $4 = $5 + $0 | 0; + HEAP32[$3 + $5 >> 2] = HEAP32[$4 >> 2] - HEAP32[$4 + -4 >> 2]; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + return; + } + memcpy($3, $0, $1 << 2); + } + + function FLAC__fixed_restore_signal($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + label$1 : { + label$2 : { + label$3 : { + switch ($2 | 0) { + case 4: + if (($1 | 0) < 1) { + break label$2 + } + $5 = HEAP32[$3 + -12 >> 2]; + $6 = HEAP32[$3 + -4 >> 2]; + $2 = 0; + while (1) { + $8 = $2 << 2; + $7 = $8 + $3 | 0; + $4 = HEAP32[$7 + -8 >> 2]; + $6 = ((HEAP32[$0 + $8 >> 2] + Math_imul($4, -6) | 0) - HEAP32[$7 + -16 >> 2] | 0) + ($5 + $6 << 2) | 0; + HEAP32[$7 >> 2] = $6; + $5 = $4; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 3: + if (($1 | 0) < 1) { + break label$2 + } + $4 = HEAP32[$3 + -12 >> 2]; + $5 = HEAP32[$3 + -4 >> 2]; + $2 = 0; + while (1) { + $6 = $2 << 2; + $7 = $6 + $3 | 0; + $8 = HEAP32[$0 + $6 >> 2] + $4 | 0; + $4 = HEAP32[$7 + -8 >> 2]; + $5 = $8 + Math_imul($5 - $4 | 0, 3) | 0; + HEAP32[$7 >> 2] = $5; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 2: + if (($1 | 0) < 1) { + break label$2 + } + $4 = HEAP32[$3 + -4 >> 2]; + $2 = 0; + while (1) { + $5 = $2 << 2; + $6 = $5 + $3 | 0; + $4 = (HEAP32[$0 + $5 >> 2] + ($4 << 1) | 0) - HEAP32[$6 + -8 >> 2] | 0; + HEAP32[$6 >> 2] = $4; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$2; + case 0: + break label$1; + case 1: + break label$3; + default: + break label$2; + }; + } + if (($1 | 0) < 1) { + break label$2 + } + $4 = HEAP32[$3 + -4 >> 2]; + $2 = 0; + while (1) { + $5 = $2 << 2; + $4 = HEAP32[$5 + $0 >> 2] + $4 | 0; + HEAP32[$3 + $5 >> 2] = $4; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + return; + } + memcpy($3, $0, $1 << 2); + } + + function __toread($0) { + var $1 = 0, $2 = 0; + $1 = HEAPU8[$0 + 74 | 0]; + HEAP8[$0 + 74 | 0] = $1 + -1 | $1; + if (HEAPU32[$0 + 20 >> 2] > HEAPU32[$0 + 28 >> 2]) { + FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($0, 0, 0) | 0 + } + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + $1 = HEAP32[$0 >> 2]; + if ($1 & 4) { + HEAP32[$0 >> 2] = $1 | 32; + return -1; + } + $2 = HEAP32[$0 + 44 >> 2] + HEAP32[$0 + 48 >> 2] | 0; + HEAP32[$0 + 8 >> 2] = $2; + HEAP32[$0 + 4 >> 2] = $2; + return $1 << 27 >> 31; + } + + function FLAC__stream_decoder_new() { + var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0; + $3 = dlcalloc(1, 8); + if ($3) { + $2 = dlcalloc(1, 504); + HEAP32[$3 >> 2] = $2; + if ($2) { + $0 = dlcalloc(1, 6160); + HEAP32[$3 + 4 >> 2] = $0; + if ($0) { + $1 = dlcalloc(1, 44); + HEAP32[$0 + 56 >> 2] = $1; + if ($1) { + HEAP32[$0 + 1128 >> 2] = 16; + $4 = dlmalloc(HEAP32[1364] << 1 & -16); + HEAP32[$0 + 1120 >> 2] = $4; + if ($4) { + HEAP32[$0 + 252 >> 2] = 0; + HEAP32[$0 + 220 >> 2] = 0; + HEAP32[$0 + 224 >> 2] = 0; + $1 = $0 + 3616 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + $1 = $0 + 3608 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + $1 = $0 + 3600 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + $1 = $0 + 3592 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + HEAP32[$0 + 60 >> 2] = 0; + HEAP32[$0 + 64 >> 2] = 0; + HEAP32[$0 + 68 >> 2] = 0; + HEAP32[$0 + 72 >> 2] = 0; + HEAP32[$0 + 76 >> 2] = 0; + HEAP32[$0 + 80 >> 2] = 0; + HEAP32[$0 + 84 >> 2] = 0; + HEAP32[$0 + 88 >> 2] = 0; + HEAP32[$0 + 92 >> 2] = 0; + HEAP32[$0 + 96 >> 2] = 0; + HEAP32[$0 + 100 >> 2] = 0; + HEAP32[$0 + 104 >> 2] = 0; + HEAP32[$0 + 108 >> 2] = 0; + HEAP32[$0 + 112 >> 2] = 0; + HEAP32[$0 + 116 >> 2] = 0; + HEAP32[$0 + 120 >> 2] = 0; + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 124 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 136 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 148 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 160 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 172 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 184 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 196 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 208 | 0); + HEAP32[$0 + 48 >> 2] = 0; + HEAP32[$0 + 52 >> 2] = 0; + memset($0 + 608 | 0, 512); + HEAP32[$0 + 1124 >> 2] = 0; + HEAP32[$0 + 608 >> 2] = 1; + HEAP32[$0 + 32 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 0; + HEAP32[$0 + 28 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$2 + 28 >> 2] = 0; + FLAC__ogg_decoder_aspect_set_defaults($2 + 32 | 0); + HEAP32[$2 >> 2] = 9; + return $3 | 0; + } + FLAC__bitreader_delete($1); + } + dlfree($0); + } + dlfree($2); + } + dlfree($3); + } + return 0; + } + + function FLAC__stream_decoder_delete($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0; + if ($0) { + FLAC__stream_decoder_finish($0); + $1 = HEAP32[$0 + 4 >> 2]; + $2 = HEAP32[$1 + 1120 >> 2]; + if ($2) { + dlfree($2); + $1 = HEAP32[$0 + 4 >> 2]; + } + FLAC__bitreader_delete(HEAP32[$1 + 56 >> 2]); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 124 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 136 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 148 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 160 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 172 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 184 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 196 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 208 | 0); + dlfree(HEAP32[$0 + 4 >> 2]); + dlfree(HEAP32[$0 >> 2]); + dlfree($0); + } + } + + function FLAC__stream_decoder_finish($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0; + $3 = 1; + if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9) { + $1 = HEAP32[$0 + 4 >> 2]; + FLAC__MD5Final($1 + 3732 | 0, $1 + 3636 | 0); + dlfree(HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2]); + HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 252 >> 2] = 0; + FLAC__bitreader_free(HEAP32[$1 + 56 >> 2]); + $3 = $0 + 4 | 0; + $1 = HEAP32[$0 + 4 >> 2]; + $2 = HEAP32[$1 + 60 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 60 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3592 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 92 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3592 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 - -64 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] - -64 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3596 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 96 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3596 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 68 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 68 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3600 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 100 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3600 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 72 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 72 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3604 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 104 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3604 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 76 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 76 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3608 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 108 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3608 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 80 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 80 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3612 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 112 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3612 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 84 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 84 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3616 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 116 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3616 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 88 >> 2]; + if ($2) { + dlfree($2 + -16 | 0); + HEAP32[HEAP32[$3 >> 2] + 88 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + $2 = HEAP32[$1 + 3620 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$3 >> 2] + 120 >> 2] = 0; + HEAP32[HEAP32[$3 >> 2] + 3620 >> 2] = 0; + $1 = HEAP32[$3 >> 2]; + } + HEAP32[$1 + 220 >> 2] = 0; + HEAP32[$1 + 224 >> 2] = 0; + if (HEAP32[$1 >> 2]) { + $1 = HEAP32[$0 >> 2] + 32 | 0; + ogg_sync_clear($1 + 368 | 0); + ogg_stream_clear($1 + 8 | 0); + $1 = HEAP32[$0 + 4 >> 2]; + } + $2 = HEAP32[$1 + 52 >> 2]; + if ($2) { + if (($2 | 0) != HEAP32[1887]) { + fclose($2); + $1 = HEAP32[$3 >> 2]; + } + HEAP32[$1 + 52 >> 2] = 0; + } + $3 = 1; + if (HEAP32[$1 + 3624 >> 2]) { + $3 = !memcmp($1 + 312 | 0, $1 + 3732 | 0, 16) + } + HEAP32[$1 + 48 >> 2] = 0; + HEAP32[$1 + 3632 >> 2] = 0; + memset($1 + 608 | 0, 512); + HEAP32[$1 + 32 >> 2] = 0; + HEAP32[$1 + 24 >> 2] = 0; + HEAP32[$1 + 28 >> 2] = 0; + HEAP32[$1 + 16 >> 2] = 0; + HEAP32[$1 + 20 >> 2] = 0; + HEAP32[$1 + 8 >> 2] = 0; + HEAP32[$1 + 12 >> 2] = 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 1124 >> 2] = 0; + HEAP32[$1 + 608 >> 2] = 1; + $1 = HEAP32[$0 >> 2]; + HEAP32[$1 + 28 >> 2] = 0; + FLAC__ogg_decoder_aspect_set_defaults($1 + 32 | 0); + HEAP32[HEAP32[$0 >> 2] >> 2] = 9; + } + return $3 | 0; + } + + function FLAC__stream_decoder_init_stream($0, $1, $2, $3, $4, $5, $6, $7, $8, $9) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + $7 = $7 | 0; + $8 = $8 | 0; + $9 = $9 | 0; + return init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, 0) | 0; + } + + function init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) { + var $11 = 0, $12 = 0; + $11 = 5; + label$1 : { + $12 = HEAP32[$0 >> 2]; + label$2 : { + if (HEAP32[$12 >> 2] != 9) { + break label$2 + } + $11 = 2; + if (!$8 | (!$1 | !$6)) { + break label$2 + } + if ($2) { + if (!$5 | (!$3 | !$4)) { + break label$2 + } + } + $11 = HEAP32[$0 + 4 >> 2]; + HEAP32[$11 >> 2] = $10; + if ($10) { + if (!FLAC__ogg_decoder_aspect_init($12 + 32 | 0)) { + break label$1 + } + $11 = HEAP32[$0 + 4 >> 2]; + } + FLAC__cpu_info($11 + 3524 | 0); + $10 = HEAP32[$0 + 4 >> 2]; + HEAP32[$10 + 44 >> 2] = 5; + HEAP32[$10 + 40 >> 2] = 6; + HEAP32[$10 + 36 >> 2] = 5; + if (!FLAC__bitreader_init(HEAP32[$10 + 56 >> 2], $0)) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + return 3; + } + $10 = HEAP32[$0 + 4 >> 2]; + HEAP32[$10 + 48 >> 2] = $9; + HEAP32[$10 + 32 >> 2] = $8; + HEAP32[$10 + 28 >> 2] = $7; + HEAP32[$10 + 24 >> 2] = $6; + HEAP32[$10 + 20 >> 2] = $5; + HEAP32[$10 + 16 >> 2] = $4; + HEAP32[$10 + 12 >> 2] = $3; + HEAP32[$10 + 8 >> 2] = $2; + HEAP32[$10 + 4 >> 2] = $1; + HEAP32[$10 + 3520 >> 2] = 0; + HEAP32[$10 + 248 >> 2] = 0; + HEAP32[$10 + 240 >> 2] = 0; + HEAP32[$10 + 244 >> 2] = 0; + HEAP32[$10 + 228 >> 2] = 0; + HEAP32[$10 + 232 >> 2] = 0; + HEAP32[$10 + 3624 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; + HEAP32[$10 + 3628 >> 2] = 1; + HEAP32[$10 + 3632 >> 2] = 0; + $11 = FLAC__stream_decoder_reset($0) ? 0 : 3; + } + return $11; + } + HEAP32[HEAP32[$0 >> 2] + 4 >> 2] = 4; + return 4; + } + + function read_callback_($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0; + label$1 : { + $3 = HEAP32[$2 + 4 >> 2]; + if (HEAP32[$3 >> 2]) { + break label$1 + } + $4 = HEAP32[$3 + 20 >> 2]; + if (!$4) { + break label$1 + } + if (!FUNCTION_TABLE[$4]($2, HEAP32[$3 + 48 >> 2])) { + break label$1 + } + HEAP32[$1 >> 2] = 0; + HEAP32[HEAP32[$2 >> 2] >> 2] = 4; + return 0; + } + label$2 : { + label$3 : { + if (HEAP32[$1 >> 2]) { + $3 = HEAP32[$2 + 4 >> 2]; + if (!(!HEAP32[$3 + 3632 >> 2] | HEAPU32[$3 + 6152 >> 2] < 21)) { + HEAP32[HEAP32[$2 >> 2] >> 2] = 7; + break label$3; + } + label$6 : { + label$7 : { + label$8 : { + label$9 : { + if (HEAP32[$3 >> 2]) { + $4 = 0; + switch (FLAC__ogg_decoder_aspect_read_callback_wrapper(HEAP32[$2 >> 2] + 32 | 0, $0, $1, $2, HEAP32[$3 + 48 >> 2]) | 0) { + case 0: + case 2: + break label$7; + case 1: + break label$8; + default: + break label$9; + }; + } + $4 = FUNCTION_TABLE[HEAP32[$3 + 4 >> 2]]($2, $0, $1, HEAP32[$3 + 48 >> 2]) | 0; + if (($4 | 0) != 2) { + break label$7 + } + } + HEAP32[HEAP32[$2 >> 2] >> 2] = 7; + break label$3; + } + $0 = 1; + if (!HEAP32[$1 >> 2]) { + break label$6 + } + break label$2; + } + $0 = 1; + if (HEAP32[$1 >> 2]) { + break label$2 + } + if (($4 | 0) == 1) { + break label$6 + } + $1 = HEAP32[$2 + 4 >> 2]; + if (HEAP32[$1 >> 2]) { + break label$2 + } + $3 = HEAP32[$1 + 20 >> 2]; + if (!$3) { + break label$2 + } + if (!FUNCTION_TABLE[$3]($2, HEAP32[$1 + 48 >> 2])) { + break label$2 + } + } + HEAP32[HEAP32[$2 >> 2] >> 2] = 4; + break label$3; + } + HEAP32[HEAP32[$2 >> 2] >> 2] = 7; + } + $0 = 0; + } + return $0 | 0; + } + + function FLAC__stream_decoder_reset($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0; + $1 = HEAP32[$0 + 4 >> 2]; + label$1 : { + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9 ? !HEAP32[$1 + 3628 >> 2] : 0) { + break label$1 + } + HEAP32[$1 + 3624 >> 2] = 0; + HEAP32[$1 + 240 >> 2] = 0; + HEAP32[$1 + 244 >> 2] = 0; + if (HEAP32[$1 >> 2]) { + $1 = HEAP32[$0 >> 2] + 32 | 0; + ogg_stream_reset($1 + 8 | 0); + ogg_sync_reset($1 + 368 | 0); + HEAP32[$1 + 408 >> 2] = 0; + HEAP32[$1 + 412 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + } + $1 = HEAP32[$1 + 56 >> 2]; + HEAP32[$1 + 8 >> 2] = 0; + HEAP32[$1 + 12 >> 2] = 0; + HEAP32[$1 + 16 >> 2] = 0; + HEAP32[$1 + 20 >> 2] = 0; + $1 = 1; + $2 = HEAP32[$0 >> 2]; + if (!$1) { + HEAP32[$2 >> 2] = 8; + return 0; + } + HEAP32[$2 >> 2] = 2; + $1 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$1 >> 2]) { + FLAC__ogg_decoder_aspect_reset($2 + 32 | 0); + $1 = HEAP32[$0 + 4 >> 2]; + } + label$6 : { + if (!HEAP32[$1 + 3628 >> 2]) { + $2 = 0; + if (HEAP32[$1 + 52 >> 2] == HEAP32[1887]) { + break label$1 + } + $3 = HEAP32[$1 + 8 >> 2]; + if (!$3) { + break label$6 + } + if ((FUNCTION_TABLE[$3]($0, 0, 0, HEAP32[$1 + 48 >> 2]) | 0) == 1) { + break label$1 + } + $1 = HEAP32[$0 + 4 >> 2]; + break label$6; + } + HEAP32[$1 + 3628 >> 2] = 0; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 0; + HEAP32[$1 + 248 >> 2] = 0; + dlfree(HEAP32[$1 + 452 >> 2]); + HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 252 >> 2] = 0; + HEAP32[$1 + 3624 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; + HEAP32[$1 + 228 >> 2] = 0; + HEAP32[$1 + 232 >> 2] = 0; + FLAC__MD5Init($1 + 3636 | 0); + $0 = HEAP32[$0 + 4 >> 2]; + HEAP32[$0 + 6152 >> 2] = 0; + HEAP32[$0 + 6136 >> 2] = 0; + HEAP32[$0 + 6140 >> 2] = 0; + $2 = 1; + } + return $2 | 0; + } + + function FLAC__stream_decoder_init_ogg_stream($0, $1, $2, $3, $4, $5, $6, $7, $8, $9) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + $7 = $7 | 0; + $8 = $8 | 0; + $9 = $9 | 0; + return init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, 1) | 0; + } + + function FLAC__stream_decoder_set_ogg_serial_number($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 9) { + $0 = $0 + 32 | 0; + HEAP32[$0 + 4 >> 2] = $1; + HEAP32[$0 >> 2] = 0; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_decoder_set_md5_checking($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 9) { + HEAP32[$0 + 28 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_decoder_set_metadata_respond($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0; + label$1 : { + if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9 | $1 >>> 0 > 126) { + break label$1 + } + $2 = 1; + $0 = HEAP32[$0 + 4 >> 2]; + HEAP32[($0 + ($1 << 2) | 0) + 608 >> 2] = 1; + if (($1 | 0) != 2) { + break label$1 + } + HEAP32[$0 + 1124 >> 2] = 0; + } + return $2 | 0; + } + + function FLAC__stream_decoder_set_metadata_respond_application($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0, $3 = 0, $4 = 0; + $2 = 0; + label$1 : { + if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9) { + break label$1 + } + $3 = HEAP32[$0 + 4 >> 2]; + $2 = 1; + if (HEAP32[$3 + 616 >> 2]) { + break label$1 + } + $2 = HEAP32[$3 + 1120 >> 2]; + label$2 : { + $4 = HEAP32[$3 + 1124 >> 2]; + label$3 : { + if (($4 | 0) != HEAP32[$3 + 1128 >> 2]) { + $3 = $2; + break label$3; + } + label$5 : { + if (!$4) { + $3 = dlrealloc($2, 0); + break label$5; + } + if ($4 + $4 >>> 0 >= $4 >>> 0) { + $3 = dlrealloc($2, $4 << 1); + if ($3) { + break label$5 + } + dlfree($2); + $3 = HEAP32[$0 + 4 >> 2]; + } + HEAP32[$3 + 1120 >> 2] = 0; + break label$2; + } + $2 = HEAP32[$0 + 4 >> 2]; + HEAP32[$2 + 1120 >> 2] = $3; + if (!$3) { + break label$2 + } + HEAP32[$2 + 1128 >> 2] = HEAP32[$2 + 1128 >> 2] << 1; + $4 = HEAP32[$2 + 1124 >> 2]; + } + $2 = $3; + $3 = HEAP32[1364] >>> 3 | 0; + memcpy($2 + Math_imul($3, $4) | 0, $1, $3); + $0 = HEAP32[$0 + 4 >> 2]; + HEAP32[$0 + 1124 >> 2] = HEAP32[$0 + 1124 >> 2] + 1; + return 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $2 = 0; + } + return $2 | 0; + } + + function FLAC__stream_decoder_set_metadata_respond_all($0) { + $0 = $0 | 0; + var $1 = 0; + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { + $1 = HEAP32[$0 + 4 >> 2]; + $0 = 0; + while (1) { + HEAP32[($1 + ($0 << 2) | 0) + 608 >> 2] = 1; + $0 = $0 + 1 | 0; + if (($0 | 0) != 128) { + continue + } + break; + }; + HEAP32[$1 + 1124 >> 2] = 0; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_decoder_set_metadata_ignore($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0; + label$1 : { + if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9 | $1 >>> 0 > 126) { + break label$1 + } + $0 = HEAP32[$0 + 4 >> 2]; + HEAP32[($0 + ($1 << 2) | 0) + 608 >> 2] = 0; + $2 = 1; + if (($1 | 0) != 2) { + break label$1 + } + HEAP32[$0 + 1124 >> 2] = 0; + } + return $2 | 0; + } + + function FLAC__stream_decoder_set_metadata_ignore_application($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0, $3 = 0, $4 = 0; + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { + $2 = HEAP32[$0 + 4 >> 2]; + if (!HEAP32[$2 + 616 >> 2]) { + return 1 + } + $3 = HEAP32[$2 + 1120 >> 2]; + label$3 : { + $4 = HEAP32[$2 + 1124 >> 2]; + label$4 : { + if (($4 | 0) != HEAP32[$2 + 1128 >> 2]) { + $2 = $3; + break label$4; + } + label$6 : { + if (!$4) { + $2 = dlrealloc($3, 0); + break label$6; + } + if ($4 + $4 >>> 0 >= $4 >>> 0) { + $2 = dlrealloc($3, $4 << 1); + if ($2) { + break label$6 + } + dlfree($3); + $2 = HEAP32[$0 + 4 >> 2]; + } + HEAP32[$2 + 1120 >> 2] = 0; + break label$3; + } + $3 = HEAP32[$0 + 4 >> 2]; + HEAP32[$3 + 1120 >> 2] = $2; + if (!$2) { + break label$3 + } + HEAP32[$3 + 1128 >> 2] = HEAP32[$3 + 1128 >> 2] << 1; + $4 = HEAP32[$3 + 1124 >> 2]; + } + $3 = $2; + $2 = HEAP32[1364] >>> 3 | 0; + memcpy($3 + Math_imul($2, $4) | 0, $1, $2); + $0 = HEAP32[$0 + 4 >> 2]; + HEAP32[$0 + 1124 >> 2] = HEAP32[$0 + 1124 >> 2] + 1; + return 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + } + return 0; + } + + function FLAC__stream_decoder_set_metadata_ignore_all($0) { + $0 = $0 | 0; + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { + memset(HEAP32[$0 + 4 >> 2] + 608 | 0, 512); + HEAP32[HEAP32[$0 + 4 >> 2] + 1124 >> 2] = 0; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_decoder_get_state($0) { + $0 = $0 | 0; + return HEAP32[HEAP32[$0 >> 2] >> 2]; + } + + function FLAC__stream_decoder_get_md5_checking($0) { + $0 = $0 | 0; + return HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; + } + + function FLAC__stream_decoder_process_single($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0; + $1 = global$0 - 16 | 0; + global$0 = $1; + $2 = 1; + label$1 : { + while (1) { + label$3 : { + label$4 : { + switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { + case 0: + if (find_metadata_($0)) { + continue + } + $2 = 0; + break label$3; + case 1: + $3 = (read_metadata_($0) | 0) != 0; + break label$1; + case 2: + if (frame_sync_($0)) { + continue + } + break label$3; + case 4: + case 7: + break label$3; + case 3: + break label$4; + default: + break label$1; + }; + } + if (!read_frame_($0, $1 + 12 | 0)) { + $2 = 0; + break label$3; + } + if (!HEAP32[$1 + 12 >> 2]) { + continue + } + } + break; + }; + $3 = $2; + } + global$0 = $1 + 16 | 0; + return $3 | 0; + } + + function find_metadata_($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + $5 = 1; + label$1 : { + while (1) { + $1 = 0; + label$3 : { + while (1) { + $6 = HEAP32[$0 + 4 >> 2]; + label$5 : { + if (HEAP32[$6 + 3520 >> 2]) { + $4 = HEAPU8[$6 + 3590 | 0]; + HEAP32[$2 + 8 >> 2] = $4; + HEAP32[$6 + 3520 >> 2] = 0; + break label$5; + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$6 + 56 >> 2], $2 + 8 | 0, 8)) { + $3 = 0; + break label$1; + } + $4 = HEAP32[$2 + 8 >> 2]; + } + if (HEAPU8[$3 + 5409 | 0] == ($4 | 0)) { + $3 = $3 + 1 | 0; + $1 = 1; + break label$3; + } + $3 = 0; + if (($1 | 0) == 3) { + break label$1 + } + if (HEAPU8[$1 + 7552 | 0] == ($4 | 0)) { + $1 = $1 + 1 | 0; + if (($1 | 0) != 3) { + continue + } + label$10 : { + label$11 : { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 24)) { + break label$11 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { + break label$11 + } + $4 = HEAP32[$2 + 12 >> 2]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { + break label$11 + } + $6 = HEAP32[$2 + 12 >> 2]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { + break label$11 + } + $7 = HEAP32[$2 + 12 >> 2]; + if (FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { + break label$10 + } + } + break label$1; + } + if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], HEAP32[$2 + 12 >> 2] & 127 | ($7 << 7 & 16256 | ($6 & 127 | $4 << 7 & 16256) << 14))) { + continue + } + break label$1; + } + break; + }; + label$12 : { + if (($4 | 0) != 255) { + break label$12 + } + HEAP8[HEAP32[$0 + 4 >> 2] + 3588 | 0] = 255; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 8 | 0, 8)) { + break label$1 + } + $1 = HEAP32[$2 + 8 >> 2]; + if (($1 | 0) == 255) { + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 3520 >> 2] = 1; + HEAP8[$1 + 3590 | 0] = 255; + break label$12; + } + if (($1 & -2) != 248) { + break label$12 + } + HEAP8[HEAP32[$0 + 4 >> 2] + 3589 | 0] = $1; + HEAP32[HEAP32[$0 >> 2] >> 2] = 3; + $3 = 1; + break label$1; + } + $1 = 0; + if (!$5) { + break label$3 + } + $5 = HEAP32[$0 + 4 >> 2]; + $1 = 0; + if (HEAP32[$5 + 3632 >> 2]) { + break label$3 + } + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 0, HEAP32[$5 + 48 >> 2]); + $1 = 0; + } + $5 = $1; + if ($3 >>> 0 < 4) { + continue + } + break; + }; + $3 = 1; + HEAP32[HEAP32[$0 >> 2] >> 2] = 1; + } + global$0 = $2 + 16 | 0; + return $3; + } + + function read_metadata_($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0; + $7 = global$0 - 192 | 0; + global$0 = $7; + label$1 : { + label$2 : { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $7 + 184 | 0, HEAP32[1391])) { + break label$2 + } + $15 = HEAP32[$7 + 184 >> 2]; + $4 = $0 + 4 | 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 180 | 0, HEAP32[1392])) { + break label$1 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 176 | 0, HEAP32[1393])) { + break label$1 + } + $6 = ($15 | 0) != 0; + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + $2 = HEAP32[$7 + 180 >> 2]; + switch ($2 | 0) { + case 3: + break label$6; + case 0: + break label$7; + default: + break label$5; + }; + } + $3 = HEAP32[$7 + 176 >> 2]; + $2 = 0; + $1 = HEAP32[$4 >> 2]; + HEAP32[$1 + 256 >> 2] = 0; + HEAP32[$1 + 264 >> 2] = $3; + HEAP32[$1 + 260 >> 2] = $6; + $5 = HEAP32[$1 + 56 >> 2]; + $1 = HEAP32[1356]; + if (!FLAC__bitreader_read_raw_uint32($5, $7, $1)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 272 >> 2] = HEAP32[$7 >> 2]; + $5 = HEAP32[1357]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $5)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 276 >> 2] = HEAP32[$7 >> 2]; + $6 = HEAP32[1358]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $6)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 280 >> 2] = HEAP32[$7 >> 2]; + $8 = HEAP32[1359]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $8)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 284 >> 2] = HEAP32[$7 >> 2]; + $9 = HEAP32[1360]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $9)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 288 >> 2] = HEAP32[$7 >> 2]; + $10 = HEAP32[1361]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $10)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 292 >> 2] = HEAP32[$7 >> 2] + 1; + $11 = HEAP32[1362]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $11)) { + break label$1 + } + HEAP32[HEAP32[$4 >> 2] + 296 >> 2] = HEAP32[$7 >> 2] + 1; + $12 = HEAP32[$4 >> 2]; + $13 = HEAP32[$12 + 56 >> 2]; + $14 = $12 + 304 | 0; + $12 = HEAP32[1363]; + if (!FLAC__bitreader_read_raw_uint64($13, $14, $12)) { + break label$1 + } + $13 = HEAP32[$4 >> 2]; + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$13 + 56 >> 2], $13 + 312 | 0, 16)) { + break label$1 + } + if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3 - (($12 + ($11 + ($10 + ($9 + ($8 + ($6 + ($1 + $5 | 0) | 0) | 0) | 0) | 0) | 0) | 0) + 128 >>> 3 | 0) | 0)) { + break label$2 + } + $1 = HEAP32[$4 >> 2]; + HEAP32[$1 + 248 >> 2] = 1; + if (!memcmp($1 + 312 | 0, 7555, 16)) { + HEAP32[$1 + 3624 >> 2] = 0 + } + if (HEAP32[$1 + 3632 >> 2] | !HEAP32[$1 + 608 >> 2]) { + break label$4 + } + $2 = HEAP32[$1 + 28 >> 2]; + if (!$2) { + break label$4 + } + FUNCTION_TABLE[$2]($0, $1 + 256 | 0, HEAP32[$1 + 48 >> 2]); + break label$4; + } + $1 = HEAP32[$4 >> 2]; + HEAP32[$1 + 252 >> 2] = 0; + $5 = HEAP32[$7 + 176 >> 2]; + HEAP32[$1 + 448 >> 2] = ($5 >>> 0) / 18; + HEAP32[$1 + 440 >> 2] = $5; + HEAP32[$1 + 436 >> 2] = $6; + HEAP32[$1 + 432 >> 2] = 3; + $1 = HEAP32[$4 >> 2]; + $2 = HEAP32[$1 + 452 >> 2]; + $3 = HEAP32[$1 + 448 >> 2]; + label$9 : { + if ($3) { + __wasm_i64_mul($3, 0, 24, 0); + if (!i64toi32_i32$HIGH_BITS) { + $1 = dlrealloc($2, Math_imul($3, 24)); + if ($1) { + HEAP32[HEAP32[$4 >> 2] + 452 >> 2] = $1; + break label$9; + } + dlfree($2); + $1 = HEAP32[$4 >> 2]; + } + HEAP32[$1 + 452 >> 2] = 0; + break label$3; + } + $1 = dlrealloc($2, 0); + HEAP32[HEAP32[$4 >> 2] + 452 >> 2] = $1; + if (!$1) { + break label$3 + } + } + $2 = HEAP32[$4 >> 2]; + $1 = 0; + label$14 : { + if (!HEAP32[$2 + 448 >> 2]) { + break label$14 + } + $6 = HEAP32[1367]; + $8 = HEAP32[1366]; + $9 = HEAP32[1365]; + $3 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_uint64(HEAP32[$2 + 56 >> 2], $7, $9)) { + break label$2 + } + $2 = HEAP32[$7 + 4 >> 2]; + $1 = Math_imul($3, 24); + $10 = HEAP32[$4 >> 2]; + $11 = $1 + HEAP32[$10 + 452 >> 2] | 0; + HEAP32[$11 >> 2] = HEAP32[$7 >> 2]; + HEAP32[$11 + 4 >> 2] = $2; + if (!FLAC__bitreader_read_raw_uint64(HEAP32[$10 + 56 >> 2], $7, $8)) { + break label$2 + } + $2 = HEAP32[$7 + 4 >> 2]; + $10 = HEAP32[$4 >> 2]; + $11 = $1 + HEAP32[$10 + 452 >> 2] | 0; + HEAP32[$11 + 8 >> 2] = HEAP32[$7 >> 2]; + HEAP32[$11 + 12 >> 2] = $2; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$10 + 56 >> 2], $7 + 188 | 0, $6)) { + break label$2 + } + $2 = HEAP32[$4 >> 2]; + HEAP32[($1 + HEAP32[$2 + 452 >> 2] | 0) + 16 >> 2] = HEAP32[$7 + 188 >> 2]; + $3 = $3 + 1 | 0; + $1 = HEAP32[$2 + 448 >> 2]; + if ($3 >>> 0 < $1 >>> 0) { + continue + } + break; + }; + $1 = Math_imul($1, -18); + } + $1 = $1 + $5 | 0; + if ($1) { + if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$2 + 56 >> 2], $1)) { + break label$2 + } + $2 = HEAP32[$4 >> 2]; + } + HEAP32[$2 + 252 >> 2] = 1; + if (HEAP32[$2 + 3632 >> 2] | !HEAP32[$2 + 620 >> 2]) { + break label$4 + } + $1 = HEAP32[$2 + 28 >> 2]; + if (!$1) { + break label$4 + } + FUNCTION_TABLE[$1]($0, $2 + 432 | 0, HEAP32[$2 + 48 >> 2]); + break label$4; + } + $3 = HEAP32[$4 >> 2]; + $8 = HEAP32[($3 + ($2 << 2) | 0) + 608 >> 2]; + $5 = HEAP32[$7 + 176 >> 2]; + $1 = memset($7, 176); + HEAP32[$1 + 8 >> 2] = $5; + HEAP32[$1 >> 2] = $2; + HEAP32[$1 + 4 >> 2] = $6; + $9 = !$8; + label$17 : { + if (($2 | 0) != 2) { + break label$17 + } + $10 = $1 + 16 | 0; + $6 = HEAP32[1364] >>> 3 | 0; + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $10, $6)) { + break label$2 + } + if ($5 >>> 0 < $6 >>> 0) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $2 = 0; + break label$1; + } + $5 = $5 - $6 | 0; + $3 = HEAP32[$4 >> 2]; + $11 = HEAP32[$3 + 1124 >> 2]; + if (!$11) { + break label$17 + } + $12 = HEAP32[$3 + 1120 >> 2]; + $2 = 0; + while (1) { + if (memcmp($12 + Math_imul($2, $6) | 0, $10, $6)) { + $2 = $2 + 1 | 0; + if (($11 | 0) != ($2 | 0)) { + continue + } + break label$17; + } + break; + }; + $9 = ($8 | 0) != 0; + } + if ($9) { + if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $5)) { + break label$2 + } + break label$4; + } + label$22 : { + label$23 : { + label$24 : { + label$25 : { + label$26 : { + label$27 : { + label$28 : { + switch (HEAP32[$1 + 180 >> 2]) { + case 1: + if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $5)) { + break label$26 + } + $6 = 0; + break label$22; + case 2: + if (!$5) { + break label$27 + } + $2 = dlmalloc($5); + HEAP32[$1 + 20 >> 2] = $2; + if (!$2) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $6 = 0; + break label$22; + } + if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, $5)) { + break label$26 + } + $6 = 0; + break label$22; + case 4: + label$35 : { + if ($5 >>> 0 < 8) { + break label$35 + } + $6 = 0; + if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[$3 + 56 >> 2], $1 + 16 | 0)) { + break label$22 + } + $5 = $5 + -8 | 0; + $2 = HEAP32[$1 + 16 >> 2]; + label$36 : { + if ($2) { + if ($5 >>> 0 < $2 >>> 0) { + HEAP32[$1 + 16 >> 2] = 0; + HEAP32[$1 + 20 >> 2] = 0; + break label$35; + } + label$39 : { + label$40 : { + if (($2 | 0) == -1) { + HEAP32[$1 + 20 >> 2] = 0; + break label$40; + } + $3 = dlmalloc($2 + 1 | 0); + HEAP32[$1 + 20 >> 2] = $3; + if ($3) { + break label$39 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$22; + } + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { + break label$22 + } + $5 = $5 - $2 | 0; + HEAP8[HEAP32[$1 + 20 >> 2] + HEAP32[$1 + 16 >> 2] | 0] = 0; + break label$36; + } + HEAP32[$1 + 20 >> 2] = 0; + } + if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 24 | 0)) { + break label$22 + } + $2 = HEAP32[$1 + 24 >> 2]; + if ($2 >>> 0 >= 100001) { + HEAP32[$1 + 24 >> 2] = 0; + break label$22; + } + if (!$2) { + break label$35 + } + $3 = safe_malloc_mul_2op_p($2, 8); + HEAP32[$1 + 28 >> 2] = $3; + if (!$3) { + break label$24 + } + if (!HEAP32[$1 + 24 >> 2]) { + break label$35 + } + HEAP32[$3 >> 2] = 0; + HEAP32[$3 + 4 >> 2] = 0; + $2 = 0; + label$43 : { + if ($5 >>> 0 < 4) { + break label$43 + } + while (1) { + if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3)) { + break label$23 + } + $5 = $5 + -4 | 0; + $8 = HEAP32[$1 + 28 >> 2]; + $9 = $2 << 3; + $3 = $8 + $9 | 0; + $6 = HEAP32[$3 >> 2]; + label$45 : { + if ($6) { + if ($5 >>> 0 < $6 >>> 0) { + break label$43 + } + label$47 : { + label$48 : { + if (($6 | 0) == -1) { + HEAP32[($8 + ($2 << 3) | 0) + 4 >> 2] = 0; + break label$48; + } + $8 = dlmalloc($6 + 1 | 0); + HEAP32[$3 + 4 >> 2] = $8; + if ($8) { + break label$47 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$23; + } + $5 = $5 - $6 | 0; + memset($8, HEAP32[$3 >> 2]); + $6 = FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], HEAP32[$3 + 4 >> 2], HEAP32[$3 >> 2]); + $8 = $9 + HEAP32[$1 + 28 >> 2] | 0; + $3 = HEAP32[$8 + 4 >> 2]; + if (!$6) { + dlfree($3); + HEAP32[(HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0) + 4 >> 2] = 0; + break label$43; + } + HEAP8[$3 + HEAP32[$8 >> 2] | 0] = 0; + break label$45; + } + HEAP32[$3 + 4 >> 2] = 0; + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 >= HEAPU32[$1 + 24 >> 2]) { + break label$35 + } + $3 = HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0; + HEAP32[$3 >> 2] = 0; + HEAP32[$3 + 4 >> 2] = 0; + if ($5 >>> 0 >= 4) { + continue + } + break; + }; + } + HEAP32[$1 + 24 >> 2] = $2; + } + if (!$5) { + break label$26 + } + if (!HEAP32[$1 + 24 >> 2]) { + $2 = $1 + 28 | 0; + dlfree(HEAP32[$2 >> 2]); + HEAP32[$2 >> 2] = 0; + } + if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $5)) { + break label$26 + } + $6 = 0; + break label$22; + case 5: + $6 = 0; + $2 = memset($1 + 16 | 0, 160); + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, HEAP32[1378] >>> 3 | 0)) { + break label$22 + } + if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 152 | 0, HEAP32[1379])) { + break label$22 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1380])) { + break label$22 + } + HEAP32[$1 + 160 >> 2] = HEAP32[$1 + 188 >> 2] != 0; + if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], HEAP32[1381])) { + break label$22 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1382])) { + break label$22 + } + $2 = HEAP32[$1 + 188 >> 2]; + HEAP32[$1 + 164 >> 2] = $2; + if (!$2) { + break label$26 + } + $2 = dlcalloc($2, 32); + HEAP32[$1 + 168 >> 2] = $2; + if (!$2) { + break label$25 + } + $9 = HEAP32[1371]; + if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2, $9)) { + break label$22 + } + $10 = HEAP32[1373] >>> 3 | 0; + $11 = HEAP32[1370]; + $12 = HEAP32[1369]; + $8 = HEAP32[1368]; + $13 = HEAP32[1377]; + $16 = HEAP32[1376]; + $17 = HEAP32[1375]; + $18 = HEAP32[1374]; + $19 = HEAP32[1372]; + $5 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $19)) { + break label$22 + } + $2 = ($5 << 5) + $2 | 0; + HEAP8[$2 + 8 | 0] = HEAP32[$1 + 188 >> 2]; + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2 + 9 | 0, $10)) { + break label$22 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $18)) { + break label$22 + } + HEAP8[$2 + 22 | 0] = HEAPU8[$2 + 22 | 0] & 254 | HEAP8[$1 + 188 | 0] & 1; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $17)) { + break label$22 + } + $3 = $2 + 22 | 0; + HEAP8[$3 | 0] = HEAPU8[$1 + 188 | 0] << 1 & 2 | HEAPU8[$3 | 0] & 253; + if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $16)) { + break label$22 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $13)) { + break label$22 + } + $3 = HEAP32[$1 + 188 >> 2]; + HEAP8[$2 + 23 | 0] = $3; + label$53 : { + $3 = $3 & 255; + if (!$3) { + break label$53 + } + $3 = dlcalloc($3, 16); + HEAP32[$2 + 24 >> 2] = $3; + label$54 : { + if ($3) { + $14 = $2 + 23 | 0; + if (!HEAPU8[$14 | 0]) { + break label$53 + } + if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $8)) { + break label$22 + } + $20 = $2 + 24 | 0; + $2 = 0; + break label$54; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$22; + } + while (1) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $12)) { + break label$22 + } + HEAP8[(($2 << 4) + $3 | 0) + 8 | 0] = HEAP32[$1 + 188 >> 2]; + if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $11)) { + break label$22 + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 >= HEAPU8[$14 | 0]) { + break label$53 + } + $3 = HEAP32[$20 >> 2]; + if (FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3 + ($2 << 4) | 0, $8)) { + continue + } + break; + }; + break label$22; + } + $5 = $5 + 1 | 0; + if ($5 >>> 0 >= HEAPU32[$1 + 164 >> 2]) { + break label$26 + } + $2 = HEAP32[$1 + 168 >> 2]; + if (FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2 + ($5 << 5) | 0, $9)) { + continue + } + break; + }; + break label$22; + case 6: + label$57 : { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$3 + 56 >> 2], $1 + 188 | 0, HEAP32[1383])) { + break label$57 + } + HEAP32[$1 + 16 >> 2] = HEAP32[$1 + 188 >> 2]; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1384])) { + break label$57 + } + label$58 : { + $2 = HEAP32[$1 + 188 >> 2]; + label$59 : { + if (($2 | 0) == -1) { + HEAP32[$1 + 20 >> 2] = 0; + break label$59; + } + $3 = dlmalloc($2 + 1 | 0); + HEAP32[$1 + 20 >> 2] = $3; + if ($3) { + break label$58 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $6 = 0; + break label$22; + } + if ($2) { + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { + break label$57 + } + $3 = HEAP32[$1 + 20 >> 2]; + $2 = HEAP32[$1 + 188 >> 2]; + } else { + $2 = 0 + } + HEAP8[$2 + $3 | 0] = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1385])) { + break label$57 + } + label$63 : { + $2 = HEAP32[$1 + 188 >> 2]; + label$64 : { + if (($2 | 0) == -1) { + HEAP32[$1 + 24 >> 2] = 0; + break label$64; + } + $3 = dlmalloc($2 + 1 | 0); + HEAP32[$1 + 24 >> 2] = $3; + if ($3) { + break label$63 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $6 = 0; + break label$22; + } + if ($2) { + if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { + break label$57 + } + $3 = HEAP32[$1 + 24 >> 2]; + $2 = HEAP32[$1 + 188 >> 2]; + } else { + $2 = 0 + } + HEAP8[$2 + $3 | 0] = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 28 | 0, HEAP32[1386])) { + break label$57 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 32 | 0, HEAP32[1387])) { + break label$57 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 36 | 0, HEAP32[1388])) { + break label$57 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 40 | 0, HEAP32[1389])) { + break label$57 + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 44 | 0, HEAP32[1390])) { + break label$57 + } + $2 = HEAP32[$1 + 44 >> 2]; + $3 = dlmalloc($2 ? $2 : 1); + HEAP32[$1 + 48 >> 2] = $3; + if (!$3) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $6 = 0; + break label$22; + } + if (!$2) { + break label$26 + } + if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { + break label$26 + } + } + $6 = 0; + break label$22; + case 0: + case 3: + break label$26; + default: + break label$28; + }; + } + label$69 : { + if ($5) { + $2 = dlmalloc($5); + HEAP32[$1 + 16 >> 2] = $2; + if ($2) { + break label$69 + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $6 = 0; + break label$22; + } + HEAP32[$1 + 16 >> 2] = 0; + break label$26; + } + if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, $5)) { + break label$26 + } + $6 = 0; + break label$22; + } + HEAP32[$1 + 20 >> 2] = 0; + } + $6 = 1; + $2 = HEAP32[$4 >> 2]; + if (HEAP32[$2 + 3632 >> 2]) { + break label$22 + } + $3 = HEAP32[$2 + 28 >> 2]; + if (!$3) { + break label$22 + } + FUNCTION_TABLE[$3]($0, $1, HEAP32[$2 + 48 >> 2]); + break label$22; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$22; + } + HEAP32[$1 + 24 >> 2] = 0; + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$22; + } + HEAP32[$1 + 24 >> 2] = $2; + $6 = 0; + } + label$71 : { + label$72 : { + switch (HEAP32[$1 + 180 >> 2] + -1 | 0) { + case 1: + $1 = HEAP32[$1 + 20 >> 2]; + if (!$1) { + break label$71 + } + dlfree($1); + break label$71; + case 3: + $2 = HEAP32[$1 + 20 >> 2]; + if ($2) { + dlfree($2) + } + $3 = HEAP32[$1 + 24 >> 2]; + if ($3) { + $2 = 0; + while (1) { + $5 = HEAP32[(HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0) + 4 >> 2]; + if ($5) { + dlfree($5); + $3 = HEAP32[$1 + 24 >> 2]; + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 < $3 >>> 0) { + continue + } + break; + }; + } + $1 = HEAP32[$1 + 28 >> 2]; + if (!$1) { + break label$71 + } + dlfree($1); + break label$71; + case 4: + $3 = HEAP32[$1 + 164 >> 2]; + if ($3) { + $2 = 0; + while (1) { + $5 = HEAP32[(HEAP32[$1 + 168 >> 2] + ($2 << 5) | 0) + 24 >> 2]; + if ($5) { + dlfree($5); + $3 = HEAP32[$1 + 164 >> 2]; + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 < $3 >>> 0) { + continue + } + break; + }; + } + $1 = HEAP32[$1 + 168 >> 2]; + if (!$1) { + break label$71 + } + dlfree($1); + break label$71; + case 5: + $2 = HEAP32[$1 + 20 >> 2]; + if ($2) { + dlfree($2) + } + $2 = HEAP32[$1 + 24 >> 2]; + if ($2) { + dlfree($2) + } + $1 = HEAP32[$1 + 48 >> 2]; + if (!$1) { + break label$71 + } + dlfree($1); + break label$71; + case 0: + break label$71; + default: + break label$72; + }; + } + $1 = HEAP32[$1 + 16 >> 2]; + if (!$1) { + break label$71 + } + dlfree($1); + } + if (!$6) { + break label$2 + } + } + $2 = 1; + if (!$15) { + break label$1 + } + label$86 : { + label$87 : { + $3 = HEAP32[$4 >> 2]; + if (HEAP32[$3 >> 2]) { + break label$87 + } + $5 = HEAP32[$3 + 12 >> 2]; + if (!$5) { + break label$87 + } + $1 = $3 + 6136 | 0; + if (FUNCTION_TABLE[$5]($0, $1, HEAP32[$3 + 48 >> 2])) { + break label$87 + } + if (!FLAC__bitreader_is_consumed_byte_aligned(HEAP32[HEAP32[$4 >> 2] + 56 >> 2])) { + break label$87 + } + $3 = HEAP32[$1 >> 2]; + $4 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + $4 = ((HEAP32[$4 + 8 >> 2] - HEAP32[$4 + 16 >> 2] << 5) + (HEAP32[$4 + 12 >> 2] << 3) | 0) - HEAP32[$4 + 20 >> 2] >>> 3 | 0; + $5 = HEAP32[$1 + 4 >> 2] - ($3 >>> 0 < $4 >>> 0) | 0; + HEAP32[$1 >> 2] = $3 - $4; + HEAP32[$1 + 4 >> 2] = $5; + break label$86; + } + $1 = HEAP32[$4 >> 2]; + HEAP32[$1 + 6136 >> 2] = 0; + HEAP32[$1 + 6140 >> 2] = 0; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + } + $2 = 0; + } + global$0 = $7 + 192 | 0; + return $2; + } + + function frame_sync_($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0; + $4 = global$0 - 16 | 0; + global$0 = $4; + label$1 : { + label$2 : { + label$3 : { + $2 = HEAP32[$0 + 4 >> 2]; + if (!HEAP32[$2 + 248 >> 2]) { + break label$3 + } + $3 = HEAP32[$2 + 308 >> 2]; + $1 = $3; + $5 = HEAP32[$2 + 304 >> 2]; + if (!($1 | $5)) { + break label$3 + } + $3 = HEAP32[$2 + 244 >> 2]; + if (($1 | 0) == ($3 | 0) & HEAPU32[$2 + 240 >> 2] < $5 >>> 0 | $3 >>> 0 < $1 >>> 0) { + break label$3 + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 4; + break label$2; + } + label$4 : { + if (FLAC__bitreader_is_consumed_byte_aligned(HEAP32[$2 + 56 >> 2])) { + break label$4 + } + $2 = HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2]; + if (FLAC__bitreader_read_raw_uint32($2, $4 + 12 | 0, FLAC__bitreader_bits_left_for_byte_alignment($2))) { + break label$4 + } + $1 = 0; + break label$1; + } + $2 = 0; + while (1) { + $3 = HEAP32[$0 + 4 >> 2]; + label$6 : { + if (HEAP32[$3 + 3520 >> 2]) { + $1 = HEAPU8[$3 + 3590 | 0]; + HEAP32[$4 + 12 >> 2] = $1; + HEAP32[$3 + 3520 >> 2] = 0; + break label$6; + } + $1 = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$3 + 56 >> 2], $4 + 12 | 0, 8)) { + break label$1 + } + $1 = HEAP32[$4 + 12 >> 2]; + } + label$8 : { + if (($1 | 0) != 255) { + break label$8 + } + HEAP8[HEAP32[$0 + 4 >> 2] + 3588 | 0] = 255; + $1 = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $4 + 12 | 0, 8)) { + break label$1 + } + $1 = HEAP32[$4 + 12 >> 2]; + if (($1 | 0) == 255) { + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 3520 >> 2] = 1; + HEAP8[$1 + 3590 | 0] = 255; + break label$8; + } + if (($1 & -2) != 248) { + break label$8 + } + HEAP8[HEAP32[$0 + 4 >> 2] + 3589 | 0] = $1; + HEAP32[HEAP32[$0 >> 2] >> 2] = 3; + break label$2; + } + $1 = $2; + $2 = 1; + if ($1) { + continue + } + $1 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$1 + 3632 >> 2]) { + continue + } + FUNCTION_TABLE[HEAP32[$1 + 32 >> 2]]($0, 0, HEAP32[$1 + 48 >> 2]); + continue; + }; + } + $1 = 1; + } + global$0 = $4 + 16 | 0; + return $1; + } + + function read_frame_($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $7 = global$0 + -64 | 0; + global$0 = $7; + HEAP32[$1 >> 2] = 0; + $2 = HEAP32[$0 + 4 >> 2]; + $4 = HEAPU16[(HEAPU8[$2 + 3588 | 0] << 1) + 1280 >> 1]; + $5 = HEAP32[$2 + 56 >> 2]; + HEAP32[$5 + 24 >> 2] = HEAPU16[((HEAPU8[$2 + 3589 | 0] ^ $4 >>> 8) << 1) + 1280 >> 1] ^ $4 << 8 & 65280; + $2 = HEAP32[$5 + 20 >> 2]; + HEAP32[$5 + 28 >> 2] = HEAP32[$5 + 16 >> 2]; + HEAP32[$5 + 32 >> 2] = $2; + $5 = HEAP32[$0 + 4 >> 2]; + HEAP8[$7 + 32 | 0] = HEAPU8[$5 + 3588 | 0]; + $2 = HEAPU8[$5 + 3589 | 0]; + HEAP32[$7 + 12 >> 2] = 2; + HEAP8[$7 + 33 | 0] = $2; + label$1 : { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$5 + 56 >> 2], $7 + 28 | 0, 8)) { + break label$1 + } + $4 = $0 + 4 | 0; + label$2 : { + label$3 : { + label$4 : { + label$5 : { + $5 = HEAP32[$7 + 28 >> 2]; + if (($5 | 0) == 255) { + break label$5 + } + HEAP8[$7 + 34 | 0] = $5; + HEAP32[$7 + 12 >> 2] = 3; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 28 | 0, 8)) { + break label$3 + } + $5 = HEAP32[$7 + 28 >> 2]; + if (($5 | 0) == 255) { + break label$5 + } + $8 = $2 >>> 1 & 1; + $2 = HEAP32[$7 + 12 >> 2]; + HEAP8[$2 + ($7 + 32 | 0) | 0] = $5; + $5 = 1; + HEAP32[$7 + 12 >> 2] = $2 + 1; + $2 = HEAPU8[$7 + 34 | 0]; + $3 = $2 >>> 4 | 0; + HEAP32[$7 + 28 >> 2] = $3; + label$6 : { + label$7 : { + label$8 : { + label$9 : { + switch ($3 - 1 | 0) { + case 7: + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 256 << $3 + -8; + break label$8; + case 1: + case 2: + case 3: + case 4: + HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 576 << $3 + -2; + break label$8; + case 5: + case 6: + break label$7; + case 0: + break label$9; + default: + break label$6; + }; + } + HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 192; + } + $3 = 0; + } + $5 = $8; + } + $6 = $2 & 15; + HEAP32[$7 + 28 >> 2] = $6; + label$12 : { + label$13 : { + label$14 : { + switch ($6 - 1 | 0) { + default: + $6 = 0; + $8 = HEAP32[$4 >> 2]; + if (HEAP32[$8 + 248 >> 2]) { + break label$13 + } + $5 = 1; + break label$12; + case 0: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 88200; + $6 = 0; + break label$12; + case 1: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 176400; + $6 = 0; + break label$12; + case 2: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 192e3; + $6 = 0; + break label$12; + case 3: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 8e3; + $6 = 0; + break label$12; + case 4: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 16e3; + $6 = 0; + break label$12; + case 5: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 22050; + $6 = 0; + break label$12; + case 6: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 24e3; + $6 = 0; + break label$12; + case 7: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 32e3; + $6 = 0; + break label$12; + case 8: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 44100; + $6 = 0; + break label$12; + case 9: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 48e3; + $6 = 0; + break label$12; + case 10: + HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 96e3; + $6 = 0; + break label$12; + case 11: + case 12: + case 13: + break label$12; + case 14: + break label$14; + }; + } + $5 = HEAP32[$4 >> 2]; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + break label$4; + } + HEAP32[$8 + 1140 >> 2] = HEAP32[$8 + 288 >> 2]; + } + $10 = HEAPU8[$7 + 35 | 0]; + $9 = $10 >>> 4 | 0; + HEAP32[$7 + 28 >> 2] = $9; + label$28 : { + label$29 : { + if ($9 & 8) { + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 1144 >> 2] = 2; + $8 = 1; + label$31 : { + switch ($9 & 7) { + case 1: + $8 = 2; + break label$29; + case 0: + break label$29; + case 2: + break label$31; + default: + break label$28; + }; + } + $8 = 3; + break label$29; + } + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 1144 >> 2] = $9 + 1; + $8 = 0; + } + HEAP32[$2 + 1148 >> 2] = $8; + $8 = $5; + } + $9 = $10 >>> 1 & 7; + HEAP32[$7 + 28 >> 2] = $9; + $5 = 1; + label$33 : { + label$34 : { + label$35 : { + switch ($9 - 1 | 0) { + default: + if (!HEAP32[$2 + 248 >> 2]) { + break label$33 + } + HEAP32[$2 + 1152 >> 2] = HEAP32[$2 + 296 >> 2]; + break label$34; + case 0: + HEAP32[$2 + 1152 >> 2] = 8; + break label$34; + case 1: + HEAP32[$2 + 1152 >> 2] = 12; + break label$34; + case 3: + HEAP32[$2 + 1152 >> 2] = 16; + break label$34; + case 4: + HEAP32[$2 + 1152 >> 2] = 20; + break label$34; + case 2: + case 6: + break label$33; + case 5: + break label$35; + }; + } + HEAP32[$2 + 1152 >> 2] = 24; + } + $5 = $8; + } + label$41 : { + if (!(!HEAP32[$2 + 248 >> 2] | HEAP32[$2 + 272 >> 2] == HEAP32[$2 + 276 >> 2] ? !(HEAP8[$7 + 33 | 0] & 1) : 0)) { + if (!FLAC__bitreader_read_utf8_uint64(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $7 + 32 | 0, $7 + 12 | 0)) { + break label$3 + } + $8 = HEAP32[$7 + 20 >> 2]; + $2 = $8; + $9 = HEAP32[$7 + 16 >> 2]; + if (($9 | 0) == -1 & ($2 | 0) == -1) { + $8 = HEAPU8[(HEAP32[$7 + 12 >> 2] + $7 | 0) + 31 | 0]; + $5 = HEAP32[$4 >> 2]; + HEAP32[$5 + 3520 >> 2] = 1; + HEAP8[$5 + 3590 | 0] = $8; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + break label$4; + } + $8 = HEAP32[$4 >> 2]; + $11 = $8 + 1160 | 0; + HEAP32[$11 >> 2] = $9; + HEAP32[$11 + 4 >> 2] = $2; + HEAP32[$8 + 1156 >> 2] = 1; + break label$41; + } + if (!FLAC__bitreader_read_utf8_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, $7 + 32 | 0, $7 + 12 | 0)) { + break label$3 + } + $8 = HEAP32[$7 + 28 >> 2]; + if (($8 | 0) == -1) { + $8 = HEAPU8[(HEAP32[$7 + 12 >> 2] + $7 | 0) + 31 | 0]; + $5 = HEAP32[$4 >> 2]; + HEAP32[$5 + 3520 >> 2] = 1; + HEAP8[$5 + 3590 | 0] = $8; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + break label$4; + } + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 1160 >> 2] = $8; + HEAP32[$2 + 1156 >> 2] = 0; + } + $2 = HEAP32[$4 >> 2]; + if ($3) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { + break label$3 + } + $2 = HEAP32[$7 + 12 >> 2]; + $8 = HEAP32[$7 + 28 >> 2]; + HEAP8[$2 + ($7 + 32 | 0) | 0] = $8; + HEAP32[$7 + 12 >> 2] = $2 + 1; + if (($3 | 0) == 7) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 8 | 0, 8)) { + break label$3 + } + $8 = HEAP32[$7 + 12 >> 2]; + $2 = HEAP32[$7 + 8 >> 2]; + HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; + HEAP32[$7 + 12 >> 2] = $8 + 1; + $8 = $2 | HEAP32[$7 + 28 >> 2] << 8; + HEAP32[$7 + 28 >> 2] = $8; + } + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 1136 >> 2] = $8 + 1; + } + if ($6) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { + break label$3 + } + $8 = HEAP32[$7 + 12 >> 2]; + $2 = HEAP32[$7 + 28 >> 2]; + HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; + HEAP32[$7 + 12 >> 2] = $8 + 1; + label$51 : { + if (($6 | 0) != 12) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 8 | 0, 8)) { + break label$3 + } + $8 = HEAP32[$7 + 12 >> 2]; + $2 = HEAP32[$7 + 8 >> 2]; + HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; + HEAP32[$7 + 12 >> 2] = $8 + 1; + $3 = $2 | HEAP32[$7 + 28 >> 2] << 8; + HEAP32[$7 + 28 >> 2] = $3; + if (($6 | 0) == 13) { + break label$51 + } + $3 = Math_imul($3, 10); + break label$51; + } + $3 = Math_imul($2, 1e3); + } + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 1140 >> 2] = $3; + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { + break label$3 + } + $8 = HEAPU8[$7 + 28 | 0]; + $3 = FLAC__crc8($7 + 32 | 0, HEAP32[$7 + 12 >> 2]); + $2 = HEAP32[$4 >> 2]; + if (($3 | 0) != ($8 | 0)) { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 1, HEAP32[$2 + 48 >> 2]) + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + break label$4; + } + HEAP32[$2 + 232 >> 2] = 0; + label$55 : { + label$56 : { + if (HEAP32[$2 + 1156 >> 2]) { + break label$56 + } + $3 = $2 + 1160 | 0; + $8 = HEAP32[$3 >> 2]; + HEAP32[$7 + 28 >> 2] = $8; + HEAP32[$2 + 1156 >> 2] = 1; + $6 = HEAP32[$2 + 228 >> 2]; + if ($6) { + (wasm2js_i32$0 = $3, wasm2js_i32$1 = __wasm_i64_mul($6, 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + HEAP32[$3 + 4 >> 2] = i64toi32_i32$HIGH_BITS; + break label$56; + } + if (HEAP32[$2 + 248 >> 2]) { + $3 = HEAP32[$2 + 272 >> 2]; + if (($3 | 0) != HEAP32[$2 + 276 >> 2]) { + break label$55 + } + $2 = $2 + 1160 | 0; + (wasm2js_i32$0 = $2, wasm2js_i32$1 = __wasm_i64_mul($3, 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + HEAP32[$2 + 4 >> 2] = i64toi32_i32$HIGH_BITS; + $8 = HEAP32[$4 >> 2]; + HEAP32[$8 + 232 >> 2] = HEAP32[$8 + 276 >> 2]; + break label$56; + } + if (!$8) { + $8 = $2 + 1160 | 0; + HEAP32[$8 >> 2] = 0; + HEAP32[$8 + 4 >> 2] = 0; + $8 = HEAP32[$4 >> 2]; + HEAP32[$8 + 232 >> 2] = HEAP32[$8 + 1136 >> 2]; + break label$56; + } + $3 = $2 + 1160 | 0; + (wasm2js_i32$0 = $3, wasm2js_i32$1 = __wasm_i64_mul(HEAP32[$2 + 1136 >> 2], 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + HEAP32[$3 + 4 >> 2] = i64toi32_i32$HIGH_BITS; + } + if (!($5 | $10 & 1)) { + $2 = HEAP32[$0 >> 2]; + break label$4; + } + $2 = HEAP32[$4 >> 2]; + } + label$61 : { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); + break label$61; + } + HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + break label$4; + } + $5 = HEAP32[$4 >> 2]; + HEAP32[$5 + 3520 >> 2] = 1; + HEAP8[$5 + 3590 | 0] = 255; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) + } + $2 = HEAP32[$0 >> 2]; + HEAP32[$2 >> 2] = 2; + } + $8 = 1; + if (HEAP32[$2 >> 2] == 2) { + break label$1 + } + $2 = HEAP32[$4 >> 2]; + $5 = HEAP32[$2 + 1144 >> 2]; + $6 = HEAP32[$2 + 1136 >> 2]; + if (!(HEAPU32[$2 + 224 >> 2] >= $5 >>> 0 ? HEAPU32[$2 + 220 >> 2] >= $6 >>> 0 : 0)) { + $3 = HEAP32[$2 + 60 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 60 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3592 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 92 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3592 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 - -64 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] - -64 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3596 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 96 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3596 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 68 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 68 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3600 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 100 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3600 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 72 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 72 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3604 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 104 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3604 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 76 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 76 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3608 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 108 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3608 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 80 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 80 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3612 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 112 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3612 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 84 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 84 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 3616 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$4 >> 2] + 116 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3616 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $3 = HEAP32[$2 + 88 >> 2]; + if ($3) { + dlfree($3 + -16 | 0); + HEAP32[HEAP32[$4 >> 2] + 88 >> 2] = 0; + $2 = HEAP32[$4 >> 2]; + } + $2 = HEAP32[$2 + 3620 >> 2]; + if ($2) { + dlfree($2); + HEAP32[HEAP32[$4 >> 2] + 120 >> 2] = 0; + HEAP32[HEAP32[$4 >> 2] + 3620 >> 2] = 0; + } + label$97 : { + if (!$5) { + break label$97 + } + if ($6 >>> 0 > 4294967291) { + break label$2 + } + $2 = $6 + 4 | 0; + if (($2 & 1073741823) != ($2 | 0)) { + break label$2 + } + $9 = $2 << 2; + $3 = 0; + while (1) { + $2 = dlmalloc($9); + if (!$2) { + break label$2 + } + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + HEAP32[$2 + 8 >> 2] = 0; + HEAP32[$2 + 12 >> 2] = 0; + $10 = $3 << 2; + HEAP32[($10 + HEAP32[$4 >> 2] | 0) + 60 >> 2] = $2 + 16; + $2 = $10 + HEAP32[$4 >> 2] | 0; + if (FLAC__memory_alloc_aligned_int32_array($6, $2 + 3592 | 0, $2 + 92 | 0)) { + $3 = $3 + 1 | 0; + if (($5 | 0) == ($3 | 0)) { + break label$97 + } + continue; + } + break; + }; + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$3; + } + $2 = HEAP32[$4 >> 2]; + HEAP32[$2 + 224 >> 2] = $5; + HEAP32[$2 + 220 >> 2] = $6; + $5 = HEAP32[$2 + 1144 >> 2]; + } + label$100 : { + if ($5) { + $17 = HEAP32[1412]; + $20 = -1 << $17 ^ -1; + $18 = HEAP32[1406]; + $19 = HEAP32[1405]; + $21 = HEAP32[1413]; + $5 = 0; + while (1) { + $3 = HEAP32[$2 + 1152 >> 2]; + label$103 : { + label$104 : { + switch (HEAP32[$2 + 1148 >> 2] + -1 | 0) { + case 0: + $3 = (($5 | 0) == 1) + $3 | 0; + break label$103; + case 1: + $3 = !$5 + $3 | 0; + break label$103; + case 2: + break label$104; + default: + break label$103; + }; + } + $3 = (($5 | 0) == 1) + $3 | 0; + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { + break label$3 + } + $2 = HEAP32[$7 + 28 >> 2]; + HEAP32[$7 + 28 >> 2] = $2 & 254; + $13 = $2 & 1; + label$107 : { + if ($13) { + if (!FLAC__bitreader_read_unary_unsigned(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 32 | 0)) { + break label$3 + } + $2 = HEAP32[$4 >> 2]; + $6 = HEAP32[$7 + 32 >> 2] + 1 | 0; + HEAP32[($2 + Math_imul($5, 292) | 0) + 1464 >> 2] = $6; + if ($3 >>> 0 <= $6 >>> 0) { + break label$3 + } + $3 = $3 - $6 | 0; + break label$107; + } + $2 = HEAP32[$4 >> 2]; + HEAP32[($2 + Math_imul($5, 292) | 0) + 1464 >> 2] = 0; + } + $6 = HEAP32[$7 + 28 >> 2]; + label$109 : { + if ($6 & 128) { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$109; + } + label$112 : { + label$113 : { + label$114 : { + switch ($6 | 0) { + case 0: + $6 = HEAP32[(($5 << 2) + $2 | 0) + 60 >> 2]; + $9 = Math_imul($5, 292) + $2 | 0; + HEAP32[$9 + 1176 >> 2] = 0; + if (!FLAC__bitreader_read_raw_int32(HEAP32[$2 + 56 >> 2], $7 + 32 | 0, $3)) { + break label$3 + } + HEAP32[$9 + 1180 >> 2] = HEAP32[$7 + 32 >> 2]; + $2 = 0; + $3 = HEAP32[$4 >> 2]; + if (!HEAP32[$3 + 1136 >> 2]) { + break label$113 + } + while (1) { + HEAP32[$6 + ($2 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; + $2 = $2 + 1 | 0; + if ($2 >>> 0 < HEAPU32[$3 + 1136 >> 2]) { + continue + } + break; + }; + break label$113; + case 2: + $6 = ($2 + 1136 | 0) + Math_imul($5, 292) | 0; + $9 = $6 + 44 | 0; + $10 = $5 << 2; + $11 = HEAP32[($10 + $2 | 0) + 92 >> 2]; + HEAP32[$9 >> 2] = $11; + HEAP32[$6 + 40 >> 2] = 1; + $6 = 0; + if (HEAP32[$2 + 1136 >> 2]) { + while (1) { + if (!FLAC__bitreader_read_raw_int32(HEAP32[$2 + 56 >> 2], $7 + 32 | 0, $3)) { + break label$3 + } + HEAP32[$11 + ($6 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; + $6 = $6 + 1 | 0; + $2 = HEAP32[$4 >> 2]; + $12 = HEAP32[$2 + 1136 >> 2]; + if ($6 >>> 0 < $12 >>> 0) { + continue + } + break; + }; + $6 = $12 << 2; + } + memcpy(HEAP32[($2 + $10 | 0) + 60 >> 2], HEAP32[$9 >> 2], $6); + break label$113; + default: + break label$114; + }; + } + if ($6 >>> 0 <= 15) { + label$121 : { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); + break label$121; + } + HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$109; + } + if ($6 >>> 0 <= 24) { + $9 = Math_imul($5, 292) + $2 | 0; + HEAP32[$9 + 1176 >> 2] = 2; + $11 = $5 << 2; + $12 = HEAP32[($11 + $2 | 0) + 92 >> 2]; + $10 = $6 >>> 1 & 7; + HEAP32[$9 + 1192 >> 2] = $10; + HEAP32[$9 + 1212 >> 2] = $12; + $6 = HEAP32[$2 + 56 >> 2]; + if ($10) { + $12 = $9 + 1196 | 0; + $2 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, $3)) { + break label$3 + } + HEAP32[$12 + ($2 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; + $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + $2 = $2 + 1 | 0; + if (($10 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $19)) { + break label$3 + } + $6 = $9 + 1180 | 0; + $3 = HEAP32[$7 + 16 >> 2]; + HEAP32[$6 >> 2] = $3; + $2 = HEAP32[$4 >> 2]; + label$126 : { + label$127 : { + if ($3 >>> 0 <= 1) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $18)) { + break label$3 + } + $2 = HEAP32[$4 >> 2]; + $3 = HEAP32[$7 + 16 >> 2]; + if (HEAP32[$2 + 1136 >> 2] >>> $3 >>> 0 >= $10 >>> 0) { + break label$127 + } + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$126; + } + label$130 : { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); + break label$130; + } + HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$126; + } + HEAP32[$9 + 1184 >> 2] = $3; + $2 = Math_imul($5, 12); + HEAP32[$9 + 1188 >> 2] = ($2 + HEAP32[$4 >> 2] | 0) + 124; + $6 = HEAP32[$6 >> 2]; + if ($6 >>> 0 < 2) { + $14 = $3; + $3 = HEAP32[$0 + 4 >> 2]; + if (!read_residual_partitioned_rice_($0, $10, $14, ($2 + $3 | 0) + 124 | 0, HEAP32[($3 + $11 | 0) + 92 >> 2], ($6 | 0) == 1)) { + break label$3 + } + } + $2 = $10 << 2; + memcpy(HEAP32[($11 + HEAP32[$4 >> 2] | 0) + 60 >> 2], $9 + 1196 | 0, $2); + $3 = HEAP32[$4 >> 2]; + $6 = $3 + $11 | 0; + FLAC__fixed_restore_signal(HEAP32[$6 + 92 >> 2], HEAP32[$3 + 1136 >> 2] - $10 | 0, $10, $2 + HEAP32[$6 + 60 >> 2] | 0); + } + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { + break label$109 + } + if ($13) { + break label$112 + } + break label$109; + } + if ($6 >>> 0 <= 63) { + label$134 : { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); + break label$134; + } + HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$109; + } + $9 = Math_imul($5, 292) + $2 | 0; + HEAP32[$9 + 1176 >> 2] = 3; + $11 = $5 << 2; + $15 = HEAP32[($11 + $2 | 0) + 92 >> 2]; + $12 = $6 >>> 1 & 31; + $10 = $12 + 1 | 0; + HEAP32[$9 + 1192 >> 2] = $10; + HEAP32[$9 + 1460 >> 2] = $15; + $6 = HEAP32[$2 + 56 >> 2]; + $2 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, $3)) { + break label$3 + } + HEAP32[($9 + ($2 << 2) | 0) + 1332 >> 2] = HEAP32[$7 + 32 >> 2]; + $15 = ($2 | 0) != ($12 | 0); + $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + $2 = $2 + 1 | 0; + if ($15) { + continue + } + break; + }; + if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $17)) { + break label$3 + } + $2 = HEAP32[$7 + 16 >> 2]; + label$137 : { + if (($2 | 0) == ($20 | 0)) { + $2 = HEAP32[$4 >> 2]; + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$137; + } + $16 = $9 + 1196 | 0; + HEAP32[$16 >> 2] = $2 + 1; + if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 32 | 0, $21)) { + break label$3 + } + $2 = HEAP32[$7 + 32 >> 2]; + if (($2 | 0) <= -1) { + $2 = HEAP32[$4 >> 2]; + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$137; + } + $15 = $9 + 1200 | 0; + HEAP32[$15 >> 2] = $2; + $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + $2 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, HEAP32[$16 >> 2])) { + break label$3 + } + HEAP32[($9 + ($2 << 2) | 0) + 1204 >> 2] = HEAP32[$7 + 32 >> 2]; + $14 = ($2 | 0) != ($12 | 0); + $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + $2 = $2 + 1 | 0; + if ($14) { + continue + } + break; + }; + if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $19)) { + break label$3 + } + $14 = $9 + 1180 | 0; + $6 = HEAP32[$7 + 16 >> 2]; + HEAP32[$14 >> 2] = $6; + $2 = HEAP32[$4 >> 2]; + label$143 : { + if ($6 >>> 0 <= 1) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $18)) { + break label$3 + } + $2 = HEAP32[$4 >> 2]; + $6 = HEAP32[$7 + 16 >> 2]; + if (HEAP32[$2 + 1136 >> 2] >>> $6 >>> 0 > $12 >>> 0) { + break label$143 + } + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$137; + } + label$146 : { + if (!HEAP32[$2 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); + break label$146; + } + HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$137; + } + HEAP32[$9 + 1184 >> 2] = $6; + $2 = Math_imul($5, 12); + HEAP32[$9 + 1188 >> 2] = ($2 + HEAP32[$4 >> 2] | 0) + 124; + $12 = HEAP32[$14 >> 2]; + if ($12 >>> 0 < 2) { + $14 = $6; + $6 = HEAP32[$0 + 4 >> 2]; + if (!read_residual_partitioned_rice_($0, $10, $14, ($2 + $6 | 0) + 124 | 0, HEAP32[($6 + $11 | 0) + 92 >> 2], ($12 | 0) == 1)) { + break label$3 + } + } + $6 = $10 << 2; + memcpy(HEAP32[(HEAP32[$4 >> 2] + $11 | 0) + 60 >> 2], $9 + 1332 | 0, $6); + label$149 : { + $12 = HEAP32[$16 >> 2]; + if ($12 + ((Math_clz32($10) ^ 31) + $3 | 0) >>> 0 <= 32) { + $2 = HEAP32[$4 >> 2]; + if ($3 >>> 0 > 16 | $12 >>> 0 > 16) { + break label$149 + } + $3 = $2 + $11 | 0; + FUNCTION_TABLE[HEAP32[$2 + 44 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); + break label$137; + } + $2 = HEAP32[$4 >> 2]; + $3 = $2 + $11 | 0; + FUNCTION_TABLE[HEAP32[$2 + 40 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); + break label$137; + } + $3 = $2 + $11 | 0; + FUNCTION_TABLE[HEAP32[$2 + 36 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); + } + if (!$13 | HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { + break label$109 + } + break label$112; + } + if (!$13) { + break label$109 + } + } + $3 = HEAP32[$4 >> 2]; + $2 = HEAP32[($3 + Math_imul($5, 292) | 0) + 1464 >> 2]; + HEAP32[$7 + 28 >> 2] = $2; + if (!HEAP32[$3 + 1136 >> 2]) { + break label$109 + } + $6 = HEAP32[($3 + ($5 << 2) | 0) + 60 >> 2]; + HEAP32[$6 >> 2] = HEAP32[$6 >> 2] << $2; + $2 = 1; + if (HEAPU32[$3 + 1136 >> 2] < 2) { + break label$109 + } + while (1) { + $9 = $6 + ($2 << 2) | 0; + HEAP32[$9 >> 2] = HEAP32[$9 >> 2] << HEAP32[$7 + 28 >> 2]; + $2 = $2 + 1 | 0; + if ($2 >>> 0 < HEAPU32[$3 + 1136 >> 2]) { + continue + } + break; + }; + } + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { + break label$100 + } + $5 = $5 + 1 | 0; + $2 = HEAP32[$4 >> 2]; + if ($5 >>> 0 < HEAPU32[$2 + 1144 >> 2]) { + continue + } + break; + }; + } + label$152 : { + if (FLAC__bitreader_is_consumed_byte_aligned(HEAP32[$2 + 56 >> 2])) { + break label$152 + } + HEAP32[$7 + 32 >> 2] = 0; + $5 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; + if (!FLAC__bitreader_read_raw_uint32($5, $7 + 32 | 0, FLAC__bitreader_bits_left_for_byte_alignment($5))) { + break label$3 + } + if (!HEAP32[$7 + 32 >> 2]) { + break label$152 + } + $5 = HEAP32[$4 >> 2]; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 0, HEAP32[$5 + 48 >> 2]) + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + } + if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { + break label$1 + } + $5 = FLAC__bitreader_get_read_crc16(HEAP32[HEAP32[$4 >> 2] + 56 >> 2]); + $8 = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 16 | 0, HEAP32[1404])) { + break label$1 + } + label$154 : { + if (($5 | 0) == HEAP32[$7 + 16 >> 2]) { + label$156 : { + label$157 : { + label$158 : { + $5 = HEAP32[$4 >> 2]; + switch (HEAP32[$5 + 1148 >> 2] + -1 | 0) { + case 2: + break label$156; + case 0: + break label$157; + case 1: + break label$158; + default: + break label$154; + }; + } + if (!HEAP32[$5 + 1136 >> 2]) { + break label$154 + } + $2 = HEAP32[$5 - -64 >> 2]; + $6 = HEAP32[$5 + 60 >> 2]; + $3 = 0; + while (1) { + $9 = $3 << 2; + $10 = $9 + $6 | 0; + HEAP32[$10 >> 2] = HEAP32[$10 >> 2] + HEAP32[$2 + $9 >> 2]; + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { + continue + } + break; + }; + break label$154; + } + if (!HEAP32[$5 + 1136 >> 2]) { + break label$154 + } + $2 = HEAP32[$5 - -64 >> 2]; + $6 = HEAP32[$5 + 60 >> 2]; + $3 = 0; + while (1) { + $9 = $3 << 2; + $10 = $9 + $2 | 0; + HEAP32[$10 >> 2] = HEAP32[$6 + $9 >> 2] - HEAP32[$10 >> 2]; + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { + continue + } + break; + }; + break label$154; + } + if (!HEAP32[$5 + 1136 >> 2]) { + break label$154 + } + $10 = HEAP32[$5 - -64 >> 2]; + $11 = HEAP32[$5 + 60 >> 2]; + $3 = 0; + while (1) { + $6 = $3 << 2; + $2 = $6 + $11 | 0; + $13 = $6 + $10 | 0; + $6 = HEAP32[$13 >> 2]; + $9 = $6 & 1 | HEAP32[$2 >> 2] << 1; + HEAP32[$2 >> 2] = $6 + $9 >> 1; + HEAP32[$13 >> 2] = $9 - $6 >> 1; + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { + continue + } + break; + }; + break label$154; + } + $5 = HEAP32[$4 >> 2]; + if (!HEAP32[$5 + 3632 >> 2]) { + FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 2, HEAP32[$5 + 48 >> 2]) + } + $2 = HEAP32[$4 >> 2]; + if (!HEAP32[$2 + 1144 >> 2]) { + break label$154 + } + $3 = 0; + while (1) { + memset(HEAP32[(($3 << 2) + $2 | 0) + 60 >> 2], HEAP32[$2 + 1136 >> 2] << 2); + $3 = $3 + 1 | 0; + $2 = HEAP32[$4 >> 2]; + if ($3 >>> 0 < HEAPU32[$2 + 1144 >> 2]) { + continue + } + break; + }; + } + HEAP32[$1 >> 2] = 1; + $2 = HEAP32[$4 >> 2]; + $1 = HEAP32[$2 + 232 >> 2]; + if ($1) { + HEAP32[$2 + 228 >> 2] = $1 + } + $1 = HEAP32[$0 >> 2]; + $6 = HEAP32[$2 + 1144 >> 2]; + HEAP32[$1 + 8 >> 2] = $6; + HEAP32[$1 + 12 >> 2] = HEAP32[$2 + 1148 >> 2]; + $13 = HEAP32[$2 + 1152 >> 2]; + HEAP32[$1 + 16 >> 2] = $13; + HEAP32[$1 + 20 >> 2] = HEAP32[$2 + 1140 >> 2]; + $5 = HEAP32[$2 + 1136 >> 2]; + HEAP32[$1 + 24 >> 2] = $5; + $1 = $2 + 1160 | 0; + $9 = HEAP32[$1 >> 2]; + $3 = HEAP32[$1 + 4 >> 2]; + $1 = $3; + $12 = $5 + $9 | 0; + if ($12 >>> 0 < $5 >>> 0) { + $1 = $1 + 1 | 0 + } + HEAP32[$2 + 240 >> 2] = $12; + HEAP32[$2 + 244 >> 2] = $1; + $10 = $2 + 60 | 0; + $11 = $2 + 1136 | 0; + label$165 : { + label$166 : { + label$167 : { + if (HEAP32[$2 + 3632 >> 2]) { + HEAP32[$2 + 6156 >> 2] = 1; + $13 = HEAP32[$2 + 6144 >> 2]; + $5 = HEAP32[$2 + 6148 >> 2]; + memcpy($2 + 3752 | 0, $11, 2384); + if (($3 | 0) == ($5 | 0) & $13 >>> 0 < $9 >>> 0 | $5 >>> 0 < $3 >>> 0 | (($1 | 0) == ($5 | 0) & $13 >>> 0 >= $12 >>> 0 | $5 >>> 0 > $1 >>> 0)) { + break label$165 + } + $3 = 0; + $1 = HEAP32[$4 >> 2]; + HEAP32[$1 + 3632 >> 2] = 0; + $5 = $13 - $9 | 0; + $4 = $5; + if ($4) { + if ($6) { + while (1) { + $9 = $3 << 2; + HEAP32[$9 + ($7 + 32 | 0) >> 2] = HEAP32[($2 + $9 | 0) + 60 >> 2] + ($4 << 2); + $3 = $3 + 1 | 0; + if (($6 | 0) != ($3 | 0)) { + continue + } + break; + } + } + HEAP32[$1 + 3752 >> 2] = HEAP32[$1 + 3752 >> 2] - $4; + $2 = $1 + 3776 | 0; + $4 = $2; + $3 = $2; + $1 = HEAP32[$2 + 4 >> 2]; + $2 = $5 + HEAP32[$2 >> 2] | 0; + if ($2 >>> 0 < $5 >>> 0) { + $1 = $1 + 1 | 0 + } + HEAP32[$3 >> 2] = $2; + HEAP32[$4 + 4 >> 2] = $1; + $1 = HEAP32[$0 + 4 >> 2]; + $1 = FUNCTION_TABLE[HEAP32[$1 + 24 >> 2]]($0, $1 + 3752 | 0, $7 + 32 | 0, HEAP32[$1 + 48 >> 2]) | 0; + break label$167; + } + $1 = FUNCTION_TABLE[HEAP32[$1 + 24 >> 2]]($0, $11, $10, HEAP32[$1 + 48 >> 2]) | 0; + break label$167; + } + label$172 : { + if (!HEAP32[$2 + 248 >> 2]) { + HEAP32[$2 + 3624 >> 2] = 0; + break label$172; + } + if (!HEAP32[$2 + 3624 >> 2]) { + break label$172 + } + if (!FLAC__MD5Accumulate($2 + 3636 | 0, $10, $6, $5, $13 + 7 >>> 3 | 0)) { + break label$166 + } + $2 = HEAP32[$4 >> 2]; + } + $1 = FUNCTION_TABLE[HEAP32[$2 + 24 >> 2]]($0, $11, $10, HEAP32[$2 + 48 >> 2]) | 0; + } + if (!$1) { + break label$165 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + } + $8 = 1; + break label$1; + } + $8 = 0; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $8 = 0; + } + global$0 = $7 - -64 | 0; + return $8; + } + + function read_residual_partitioned_rice_($0, $1, $2, $3, $4, $5) { + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; + $6 = global$0 - 16 | 0; + global$0 = $6; + $7 = HEAP32[HEAP32[$0 + 4 >> 2] + 1136 >> 2]; + $11 = HEAP32[($5 ? 5644 : 5640) >> 2]; + $12 = HEAP32[($5 ? 5632 : 5628) >> 2]; + label$1 : { + label$2 : { + if (FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($3, $2 >>> 0 > 6 ? $2 : 6)) { + $8 = $2 ? $7 >>> $2 | 0 : $7 - $1 | 0; + $13 = HEAP32[1409]; + if (!$2) { + break label$2 + } + $5 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $12)) { + $7 = 0; + break label$1; + } + $9 = $10 << 2; + HEAP32[$9 + HEAP32[$3 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; + label$6 : { + if (HEAPU32[$6 + 12 >> 2] < $11 >>> 0) { + $7 = 0; + HEAP32[$9 + HEAP32[$3 + 4 >> 2] >> 2] = 0; + $9 = $8 - ($10 ? 0 : $1) | 0; + if (!FLAC__bitreader_read_rice_signed_block(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], ($5 << 2) + $4 | 0, $9, HEAP32[$6 + 12 >> 2])) { + break label$1 + } + $5 = $5 + $9 | 0; + break label$6; + } + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $13)) { + $7 = 0; + break label$1; + } + HEAP32[$9 + HEAP32[$3 + 4 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; + $7 = $10 ? 0 : $1; + if ($7 >>> 0 >= $8 >>> 0) { + break label$6 + } + while (1) { + if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 8 | 0, HEAP32[$6 + 12 >> 2])) { + $7 = 0; + break label$1; + } + HEAP32[($5 << 2) + $4 >> 2] = HEAP32[$6 + 8 >> 2]; + $5 = $5 + 1 | 0; + $7 = $7 + 1 | 0; + if (($8 | 0) != ($7 | 0)) { + continue + } + break; + }; + } + $7 = 1; + $10 = $10 + 1 | 0; + if (!($10 >>> $2)) { + continue + } + break; + }; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $7 = 0; + break label$1; + } + $7 = 0; + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $12)) { + break label$1 + } + HEAP32[HEAP32[$3 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; + label$11 : { + if (HEAPU32[$6 + 12 >> 2] >= $11 >>> 0) { + if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $13)) { + break label$1 + } + HEAP32[HEAP32[$3 + 4 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; + if (!$8) { + break label$11 + } + $5 = 0; + while (1) { + if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 8 | 0, HEAP32[$6 + 12 >> 2])) { + $7 = 0; + break label$1; + } + HEAP32[($5 << 2) + $4 >> 2] = HEAP32[$6 + 8 >> 2]; + $5 = $5 + 1 | 0; + $7 = $7 + 1 | 0; + if (($8 | 0) != ($7 | 0)) { + continue + } + break; + }; + break label$11; + } + HEAP32[HEAP32[$3 + 4 >> 2] >> 2] = 0; + if (!FLAC__bitreader_read_rice_signed_block(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $4, $8, HEAP32[$6 + 12 >> 2])) { + break label$1 + } + } + $7 = 1; + } + global$0 = $6 + 16 | 0; + return $7; + } + + function FLAC__stream_decoder_process_until_end_of_metadata($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0; + label$1 : { + label$2 : { + while (1) { + label$4 : { + $1 = 1; + label$5 : { + switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { + case 0: + if (find_metadata_($0)) { + continue + } + break label$4; + case 2: + case 3: + case 4: + case 7: + break label$2; + case 1: + break label$5; + default: + break label$1; + }; + } + if (read_metadata_($0)) { + continue + } + } + break; + }; + $1 = 0; + } + $2 = $1; + } + return $2 | 0; + } + + function FLAC__stream_decoder_process_until_end_of_stream($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0; + $1 = global$0 - 16 | 0; + global$0 = $1; + $2 = 1; + label$1 : { + label$2 : { + while (1) { + label$4 : { + label$5 : { + switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { + case 0: + if (find_metadata_($0)) { + continue + } + break label$4; + case 1: + if (read_metadata_($0)) { + continue + } + break label$4; + case 2: + if (frame_sync_($0)) { + continue + } + break label$2; + case 4: + case 7: + break label$2; + case 3: + break label$5; + default: + break label$1; + }; + } + if (read_frame_($0, $1 + 12 | 0)) { + continue + } + } + break; + }; + $2 = 0; + } + $3 = $2; + } + global$0 = $1 + 16 | 0; + return $3 | 0; + } + + function read_callback_proxy_($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $0 = FUNCTION_TABLE[HEAP32[HEAP32[$0 + 4 >> 2] + 4 >> 2]]($0, $1, $2, $3) | 0; + if ($0 >>> 0 <= 2) { + return HEAP32[($0 << 2) + 7572 >> 2] + } + return 5; + } + + function FLAC__bitwriter_free($0) { + var $1 = 0; + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + } + + function FLAC__bitwriter_init($0) { + var $1 = 0; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 8192; + HEAP32[$0 + 12 >> 2] = 0; + $1 = $0; + $0 = dlmalloc(32768); + HEAP32[$1 >> 2] = $0; + return ($0 | 0) != 0; + } + + function FLAC__bitwriter_clear($0) { + HEAP32[$0 + 12 >> 2] = 0; + HEAP32[$0 + 16 >> 2] = 0; + } + + function FLAC__bitwriter_get_write_crc16($0, $1) { + var $2 = 0, $3 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + $3 = 0; + label$1 : { + if (!FLAC__bitwriter_get_buffer($0, $2 + 12 | 0, $2 + 8 | 0)) { + break label$1 + } + (wasm2js_i32$0 = $1, wasm2js_i32$1 = FLAC__crc16(HEAP32[$2 + 12 >> 2], HEAP32[$2 + 8 >> 2])), HEAP16[wasm2js_i32$0 >> 1] = wasm2js_i32$1; + $3 = 1; + } + global$0 = $2 + 16 | 0; + return $3; + } + + function FLAC__bitwriter_get_buffer($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $5 = HEAP32[$0 + 16 >> 2]; + label$1 : { + if ($5 & 7) { + break label$1 + } + label$2 : { + if (!$5) { + $4 = HEAP32[$0 >> 2]; + $3 = 0; + break label$2; + } + $6 = HEAP32[$0 + 12 >> 2]; + label$4 : { + if (($6 | 0) != HEAP32[$0 + 8 >> 2]) { + break label$4 + } + $4 = $5 + 63 >>> 5 | 0; + $3 = $4 + $6 | 0; + if ($3 >>> 0 <= $6 >>> 0) { + break label$4 + } + $6 = 0; + $5 = HEAP32[$0 >> 2]; + $7 = $3; + $3 = $4 & 1023; + $3 = $7 + ($3 ? 1024 - $3 | 0 : 0) | 0; + label$5 : { + if ($3) { + if (($3 | 0) != ($3 & 1073741823)) { + break label$1 + } + $4 = dlrealloc($5, $3 << 2); + if ($4) { + break label$5 + } + dlfree($5); + return 0; + } + $4 = dlrealloc($5, 0); + if (!$4) { + break label$1 + } + } + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 >> 2] = $4; + $6 = HEAP32[$0 + 12 >> 2]; + $5 = HEAP32[$0 + 16 >> 2]; + } + $4 = HEAP32[$0 >> 2]; + $3 = HEAP32[$0 + 4 >> 2] << 32 - $5; + HEAP32[$4 + ($6 << 2) >> 2] = $3 << 24 | $3 << 8 & 16711680 | ($3 >>> 8 & 65280 | $3 >>> 24); + $3 = HEAP32[$0 + 16 >> 2] >>> 3 | 0; + } + HEAP32[$1 >> 2] = $4; + HEAP32[$2 >> 2] = $3 + (HEAP32[$0 + 12 >> 2] << 2); + $6 = 1; + } + return $6; + } + + function FLAC__bitwriter_get_write_crc8($0, $1) { + var $2 = 0, $3 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + $3 = 0; + label$1 : { + if (!FLAC__bitwriter_get_buffer($0, $2 + 12 | 0, $2 + 8 | 0)) { + break label$1 + } + (wasm2js_i32$0 = $1, wasm2js_i32$1 = FLAC__crc8(HEAP32[$2 + 12 >> 2], HEAP32[$2 + 8 >> 2])), HEAP8[wasm2js_i32$0 | 0] = wasm2js_i32$1; + $3 = 1; + } + global$0 = $2 + 16 | 0; + return $3; + } + + function FLAC__bitwriter_write_zeroes($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0; + label$1 : { + label$2 : { + if (!$1) { + break label$2 + } + $2 = HEAP32[$0 + 8 >> 2]; + $3 = HEAP32[$0 + 12 >> 2]; + label$3 : { + if ($2 >>> 0 > $3 + $1 >>> 0) { + break label$3 + } + $4 = $3 + ((HEAP32[$0 + 16 >> 2] + $1 | 0) + 31 >>> 5 | 0) | 0; + if ($4 >>> 0 <= $2 >>> 0) { + break label$3 + } + $3 = 0; + $5 = HEAP32[$0 >> 2]; + $2 = $4 - $2 & 1023; + $2 = $4 + ($2 ? 1024 - $2 | 0 : 0) | 0; + label$4 : { + if ($2) { + if (($2 | 0) != ($2 & 1073741823)) { + break label$1 + } + $4 = dlrealloc($5, $2 << 2); + if ($4) { + break label$4 + } + dlfree($5); + return 0; + } + $4 = dlrealloc($5, 0); + if (!$4) { + break label$1 + } + } + HEAP32[$0 + 8 >> 2] = $2; + HEAP32[$0 >> 2] = $4; + } + $2 = HEAP32[$0 + 16 >> 2]; + if ($2) { + $4 = $2; + $2 = 32 - $2 | 0; + $3 = $2 >>> 0 < $1 >>> 0 ? $2 : $1; + $5 = $4 + $3 | 0; + HEAP32[$0 + 16 >> 2] = $5; + $2 = HEAP32[$0 + 4 >> 2] << $3; + HEAP32[$0 + 4 >> 2] = $2; + if (($5 | 0) != 32) { + break label$2 + } + $5 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $5 + 1; + HEAP32[HEAP32[$0 >> 2] + ($5 << 2) >> 2] = $2 << 8 & 16711680 | $2 << 24 | ($2 >>> 8 & 65280 | $2 >>> 24); + HEAP32[$0 + 16 >> 2] = 0; + $1 = $1 - $3 | 0; + } + if ($1 >>> 0 >= 32) { + $2 = HEAP32[$0 >> 2]; + while (1) { + $3 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $3 + 1; + HEAP32[$2 + ($3 << 2) >> 2] = 0; + $1 = $1 + -32 | 0; + if ($1 >>> 0 > 31) { + continue + } + break; + }; + } + if (!$1) { + break label$2 + } + HEAP32[$0 + 16 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = 0; + } + $3 = 1; + } + return $3; + } + + function FLAC__bitwriter_write_raw_uint32($0, $1, $2) { + var $3 = 0; + label$1 : { + if ($2 >>> 0 <= 31) { + $3 = 0; + if ($1 >>> $2) { + break label$1 + } + } + $3 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $2); + } + return $3; + } + + function FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + label$1 : { + if (!$0 | $2 >>> 0 > 32) { + break label$1 + } + $4 = HEAP32[$0 >> 2]; + if (!$4) { + break label$1 + } + $6 = 1; + if (!$2) { + break label$1 + } + $7 = HEAP32[$0 + 8 >> 2]; + $3 = HEAP32[$0 + 12 >> 2]; + label$2 : { + if ($7 >>> 0 > $3 + $2 >>> 0) { + $3 = $4; + break label$2; + } + $5 = $3 + ((HEAP32[$0 + 16 >> 2] + $2 | 0) + 31 >>> 5 | 0) | 0; + if ($5 >>> 0 <= $7 >>> 0) { + $3 = $4; + break label$2; + } + $6 = 0; + $3 = $5 - $7 & 1023; + $5 = $5 + ($3 ? 1024 - $3 | 0 : 0) | 0; + label$5 : { + if ($5) { + if (($5 | 0) != ($5 & 1073741823)) { + break label$1 + } + $3 = dlrealloc($4, $5 << 2); + if ($3) { + break label$5 + } + dlfree($4); + return 0; + } + $3 = dlrealloc($4, 0); + if (!$3) { + break label$1 + } + } + HEAP32[$0 + 8 >> 2] = $5; + HEAP32[$0 >> 2] = $3; + } + $4 = HEAP32[$0 + 16 >> 2]; + $5 = 32 - $4 | 0; + if ($5 >>> 0 > $2 >>> 0) { + HEAP32[$0 + 16 >> 2] = $2 + $4; + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] << $2 | $1; + return 1; + } + if ($4) { + $4 = $2 - $5 | 0; + HEAP32[$0 + 16 >> 2] = $4; + $2 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $2 + 1; + $3 = ($2 << 2) + $3 | 0; + $2 = HEAP32[$0 + 4 >> 2] << $5 | $1 >>> $4; + HEAP32[$3 >> 2] = $2 << 24 | $2 << 8 & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); + HEAP32[$0 + 4 >> 2] = $1; + return 1; + } + $6 = 1; + $2 = $0; + $0 = HEAP32[$0 + 12 >> 2]; + HEAP32[$2 + 12 >> 2] = $0 + 1; + HEAP32[($0 << 2) + $3 >> 2] = $1 << 8 & 16711680 | $1 << 24 | ($1 >>> 8 & 65280 | $1 >>> 24); + } + return $6; + } + + function FLAC__bitwriter_write_raw_int32($0, $1, $2) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 >>> 0 < 32 ? -1 << $2 ^ -1 : -1) & $1, $2); + } + + function FLAC__bitwriter_write_raw_uint64($0, $1, $2, $3) { + var $4 = 0; + label$1 : { + if ($3 >>> 0 >= 33) { + $3 = $3 + -32 | 0; + if ($2 >>> $3 | 0 ? $3 >>> 0 <= 31 : 0) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $2, $3)) { + break label$1 + } + return (FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 32) | 0) != 0; + } + if (($3 | 0) != 32) { + if ($1 >>> $3) { + break label$1 + } + } + $4 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $3); + } + return $4; + } + + function FLAC__bitwriter_write_raw_uint32_little_endian($0, $1) { + var $2 = 0; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 255, 8)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 8 & 255, 8)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 16 & 255, 8)) { + break label$1 + } + $2 = (FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 | 0, 8) | 0) != 0; + } + return $2; + } + + function FLAC__bitwriter_write_byte_block($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0; + $3 = HEAP32[$0 + 8 >> 2]; + $4 = HEAP32[$0 + 12 >> 2]; + label$1 : { + label$2 : { + if ($3 >>> 0 > ($4 + ($2 >>> 2 | 0) | 0) + 1 >>> 0) { + break label$2 + } + $5 = $4 + ((HEAP32[$0 + 16 >> 2] + ($2 << 3) | 0) + 31 >>> 5 | 0) | 0; + if ($5 >>> 0 <= $3 >>> 0) { + break label$2 + } + $4 = 0; + $6 = HEAP32[$0 >> 2]; + $3 = $5 - $3 & 1023; + $3 = $5 + ($3 ? 1024 - $3 | 0 : 0) | 0; + label$3 : { + if ($3) { + if (($3 | 0) != ($3 & 1073741823)) { + break label$1 + } + $5 = dlrealloc($6, $3 << 2); + if ($5) { + break label$3 + } + dlfree($6); + return 0; + } + $5 = dlrealloc($6, 0); + if (!$5) { + break label$1 + } + } + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 >> 2] = $5; + } + $4 = 1; + if (!$2) { + break label$1 + } + $4 = 0; + label$5 : { + while (1) { + if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, HEAPU8[$1 + $4 | 0], 8)) { + break label$5 + } + $4 = $4 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + return 1; + } + $4 = 0; + } + return $4; + } + + function FLAC__bitwriter_write_unary_unsigned($0, $1) { + if ($1 >>> 0 <= 31) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, 1, $1 + 1 | 0) + } + if (!FLAC__bitwriter_write_zeroes($0, $1)) { + return 0 + } + return (FLAC__bitwriter_write_raw_uint32_nocheck($0, 1, 1) | 0) != 0; + } + + function FLAC__bitwriter_write_rice_signed_block($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0; + $4 = 1; + label$1 : { + if (!$2) { + break label$1 + } + $10 = $3 + 1 | 0; + $11 = -1 << $3; + $12 = -1 >>> 31 - $3 | 0; + while (1) { + $6 = HEAP32[$1 >> 2]; + $9 = $6 << 1 ^ $6 >> 31; + $6 = $9 >>> $3 | 0; + $4 = $10 + $6 | 0; + label$3 : { + label$4 : { + $5 = HEAP32[$0 + 16 >> 2]; + if (!$5) { + break label$4 + } + $7 = $4 + $5 | 0; + if ($7 >>> 0 > 31) { + break label$4 + } + HEAP32[$0 + 16 >> 2] = $7; + HEAP32[$0 + 4 >> 2] = ($9 | $11) & $12 | HEAP32[$0 + 4 >> 2] << $4; + break label$3; + } + $8 = HEAP32[$0 + 8 >> 2]; + $7 = HEAP32[$0 + 12 >> 2]; + label$5 : { + if ($8 >>> 0 > ($7 + ($5 + $6 | 0) | 0) + 1 >>> 0) { + break label$5 + } + $4 = $7 + (($4 + $5 | 0) + 31 >>> 5 | 0) | 0; + if ($4 >>> 0 <= $8 >>> 0) { + break label$5 + } + $7 = HEAP32[$0 >> 2]; + $5 = $4 - $8 & 1023; + $5 = $4 + ($5 ? 1024 - $5 | 0 : 0) | 0; + label$6 : { + if ($5) { + $4 = 0; + if (($5 | 0) != ($5 & 1073741823)) { + break label$1 + } + $8 = dlrealloc($7, $5 << 2); + if ($8) { + break label$6 + } + dlfree($7); + return 0; + } + $8 = dlrealloc($7, 0); + $4 = 0; + if (!$8) { + break label$1 + } + } + HEAP32[$0 + 8 >> 2] = $5; + HEAP32[$0 >> 2] = $8; + } + label$8 : { + if (!$6) { + break label$8 + } + $4 = HEAP32[$0 + 16 >> 2]; + if ($4) { + $5 = HEAP32[$0 + 4 >> 2]; + $7 = 32 - $4 | 0; + if ($6 >>> 0 < $7 >>> 0) { + HEAP32[$0 + 16 >> 2] = $4 + $6; + HEAP32[$0 + 4 >> 2] = $5 << $6; + break label$8; + } + $4 = $5 << $7; + HEAP32[$0 + 4 >> 2] = $4; + $5 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $5 + 1; + HEAP32[HEAP32[$0 >> 2] + ($5 << 2) >> 2] = $4 << 8 & 16711680 | $4 << 24 | ($4 >>> 8 & 65280 | $4 >>> 24); + HEAP32[$0 + 16 >> 2] = 0; + $6 = $6 - $7 | 0; + } + if ($6 >>> 0 >= 32) { + $4 = HEAP32[$0 >> 2]; + while (1) { + $5 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $5 + 1; + HEAP32[$4 + ($5 << 2) >> 2] = 0; + $6 = $6 + -32 | 0; + if ($6 >>> 0 > 31) { + continue + } + break; + }; + } + if (!$6) { + break label$8 + } + HEAP32[$0 + 16 >> 2] = $6; + HEAP32[$0 + 4 >> 2] = 0; + } + $6 = ($9 | $11) & $12; + $4 = HEAP32[$0 + 4 >> 2]; + $7 = HEAP32[$0 + 16 >> 2]; + $5 = 32 - $7 | 0; + if ($10 >>> 0 < $5 >>> 0) { + HEAP32[$0 + 16 >> 2] = $7 + $10; + HEAP32[$0 + 4 >> 2] = $6 | $4 << $10; + break label$3; + } + $7 = $10 - $5 | 0; + HEAP32[$0 + 16 >> 2] = $7; + $9 = HEAP32[$0 + 12 >> 2]; + HEAP32[$0 + 12 >> 2] = $9 + 1; + $4 = $4 << $5 | $6 >>> $7; + HEAP32[HEAP32[$0 >> 2] + ($9 << 2) >> 2] = $4 << 24 | $4 << 8 & 16711680 | ($4 >>> 8 & 65280 | $4 >>> 24); + HEAP32[$0 + 4 >> 2] = $6; + } + $1 = $1 + 4 | 0; + $2 = $2 + -1 | 0; + if ($2) { + continue + } + break; + }; + $4 = 1; + } + return $4; + } + + function FLAC__bitwriter_write_utf8_uint32($0, $1) { + if (($1 | 0) >= 0) { + if ($1 >>> 0 <= 127) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 8) + } + if ($1 >>> 0 <= 2047) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 | 192, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if ($1 >>> 0 <= 65535) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 | 224, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if ($1 >>> 0 <= 2097151) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 | 240, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if ($1 >>> 0 <= 67108863) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 | 248, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + $0 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 30 | 252, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1; + } else { + $0 = 0 + } + return $0; + } + + function FLAC__bitwriter_write_utf8_uint64($0, $1, $2) { + if (($2 | 0) == 15 | $2 >>> 0 < 15) { + if (!$2 & $1 >>> 0 <= 127 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 8) + } + if (!$2 & $1 >>> 0 <= 2047 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 63) << 26 | $1 >>> 6 | 192, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if (!$2 & $1 >>> 0 <= 65535 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 4095) << 20 | $1 >>> 12 | 224, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if (!$2 & $1 >>> 0 <= 2097151 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 262143) << 14 | $1 >>> 18 | 240, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if (!$2 & $1 >>> 0 <= 67108863 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 16777215) << 8 | $1 >>> 24 | 248, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + if (!$2 & $1 >>> 0 <= 2147483647 | $2 >>> 0 < 0) { + return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 1073741823) << 2 | $1 >>> 30 | 252, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 + } + $0 = FLAC__bitwriter_write_raw_uint32_nocheck($0, 254, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 1073741823) << 2 | $1 >>> 30 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1; + } else { + $0 = 0 + } + return $0; + } + + function FLAC__ogg_encoder_aspect_init($0) { + if (ogg_stream_init($0 + 8 | 0, HEAP32[$0 >> 2])) { + $0 = 0 + } else { + HEAP32[$0 + 392 >> 2] = 0; + HEAP32[$0 + 396 >> 2] = 0; + HEAP32[$0 + 384 >> 2] = 0; + HEAP32[$0 + 388 >> 2] = 1; + $0 = 1; + } + return $0; + } + + function FLAC__ogg_encoder_aspect_set_defaults($0) { + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + } + + function FLAC__ogg_encoder_aspect_write_callback_wrapper($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; + $9 = global$0 - 96 | 0; + global$0 = $9; + label$1 : { + label$2 : { + if (HEAP32[$0 + 384 >> 2]) { + HEAP32[$9 + 72 >> 2] = 0; + HEAP32[$9 + 76 >> 2] = 0; + $12 = $9 + 80 | 0; + $11 = $12; + HEAP32[$11 >> 2] = 0; + HEAP32[$11 + 4 >> 2] = 0; + HEAP32[$9 + 88 >> 2] = 0; + HEAP32[$9 + 92 >> 2] = 0; + HEAP32[$9 + 64 >> 2] = 0; + HEAP32[$9 + 68 >> 2] = 0; + $10 = HEAP32[$0 + 396 >> 2]; + $11 = $3; + $13 = HEAP32[$0 + 392 >> 2]; + $14 = $11 + $13 | 0; + if ($14 >>> 0 < $13 >>> 0) { + $10 = $10 + 1 | 0 + } + HEAP32[$12 >> 2] = $14; + HEAP32[$12 + 4 >> 2] = $10; + label$4 : { + label$5 : { + if (HEAP32[$0 + 388 >> 2]) { + if (($2 | 0) != 38) { + break label$4 + } + HEAP8[$9 | 0] = HEAPU8[7536]; + $2 = HEAP32[2721]; + $2 = HEAPU8[$2 | 0] | HEAPU8[$2 + 1 | 0] << 8 | (HEAPU8[$2 + 2 | 0] << 16 | HEAPU8[$2 + 3 | 0] << 24); + HEAP8[$9 + 5 | 0] = 1; + HEAP8[$9 + 6 | 0] = 0; + HEAP8[$9 + 1 | 0] = $2; + HEAP8[$9 + 2 | 0] = $2 >>> 8; + HEAP8[$9 + 3 | 0] = $2 >>> 16; + HEAP8[$9 + 4 | 0] = $2 >>> 24; + $10 = HEAP32[$0 + 4 >> 2]; + $2 = HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24); + HEAP8[$9 + 9 | 0] = $2; + HEAP8[$9 + 10 | 0] = $2 >>> 8; + HEAP8[$9 + 11 | 0] = $2 >>> 16; + HEAP8[$9 + 12 | 0] = $2 >>> 24; + HEAP8[$9 + 8 | 0] = $10; + HEAP8[$9 + 7 | 0] = $10 >>> 8; + $2 = HEAPU8[$1 + 34 | 0] | HEAPU8[$1 + 35 | 0] << 8 | (HEAPU8[$1 + 36 | 0] << 16 | HEAPU8[$1 + 37 | 0] << 24); + $10 = HEAPU8[$1 + 30 | 0] | HEAPU8[$1 + 31 | 0] << 8 | (HEAPU8[$1 + 32 | 0] << 16 | HEAPU8[$1 + 33 | 0] << 24); + HEAP8[$9 + 43 | 0] = $10; + HEAP8[$9 + 44 | 0] = $10 >>> 8; + HEAP8[$9 + 45 | 0] = $10 >>> 16; + HEAP8[$9 + 46 | 0] = $10 >>> 24; + HEAP8[$9 + 47 | 0] = $2; + HEAP8[$9 + 48 | 0] = $2 >>> 8; + HEAP8[$9 + 49 | 0] = $2 >>> 16; + HEAP8[$9 + 50 | 0] = $2 >>> 24; + $2 = HEAPU8[$1 + 28 | 0] | HEAPU8[$1 + 29 | 0] << 8 | (HEAPU8[$1 + 30 | 0] << 16 | HEAPU8[$1 + 31 | 0] << 24); + $10 = HEAPU8[$1 + 24 | 0] | HEAPU8[$1 + 25 | 0] << 8 | (HEAPU8[$1 + 26 | 0] << 16 | HEAPU8[$1 + 27 | 0] << 24); + HEAP8[$9 + 37 | 0] = $10; + HEAP8[$9 + 38 | 0] = $10 >>> 8; + HEAP8[$9 + 39 | 0] = $10 >>> 16; + HEAP8[$9 + 40 | 0] = $10 >>> 24; + HEAP8[$9 + 41 | 0] = $2; + HEAP8[$9 + 42 | 0] = $2 >>> 8; + HEAP8[$9 + 43 | 0] = $2 >>> 16; + HEAP8[$9 + 44 | 0] = $2 >>> 24; + $2 = HEAPU8[$1 + 20 | 0] | HEAPU8[$1 + 21 | 0] << 8 | (HEAPU8[$1 + 22 | 0] << 16 | HEAPU8[$1 + 23 | 0] << 24); + $10 = HEAPU8[$1 + 16 | 0] | HEAPU8[$1 + 17 | 0] << 8 | (HEAPU8[$1 + 18 | 0] << 16 | HEAPU8[$1 + 19 | 0] << 24); + HEAP8[$9 + 29 | 0] = $10; + HEAP8[$9 + 30 | 0] = $10 >>> 8; + HEAP8[$9 + 31 | 0] = $10 >>> 16; + HEAP8[$9 + 32 | 0] = $10 >>> 24; + HEAP8[$9 + 33 | 0] = $2; + HEAP8[$9 + 34 | 0] = $2 >>> 8; + HEAP8[$9 + 35 | 0] = $2 >>> 16; + HEAP8[$9 + 36 | 0] = $2 >>> 24; + $2 = HEAPU8[$1 + 12 | 0] | HEAPU8[$1 + 13 | 0] << 8 | (HEAPU8[$1 + 14 | 0] << 16 | HEAPU8[$1 + 15 | 0] << 24); + $10 = HEAPU8[$1 + 8 | 0] | HEAPU8[$1 + 9 | 0] << 8 | (HEAPU8[$1 + 10 | 0] << 16 | HEAPU8[$1 + 11 | 0] << 24); + HEAP8[$9 + 21 | 0] = $10; + HEAP8[$9 + 22 | 0] = $10 >>> 8; + HEAP8[$9 + 23 | 0] = $10 >>> 16; + HEAP8[$9 + 24 | 0] = $10 >>> 24; + HEAP8[$9 + 25 | 0] = $2; + HEAP8[$9 + 26 | 0] = $2 >>> 8; + HEAP8[$9 + 27 | 0] = $2 >>> 16; + HEAP8[$9 + 28 | 0] = $2 >>> 24; + $2 = HEAPU8[$1 + 4 | 0] | HEAPU8[$1 + 5 | 0] << 8 | (HEAPU8[$1 + 6 | 0] << 16 | HEAPU8[$1 + 7 | 0] << 24); + $1 = HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24); + HEAP8[$9 + 13 | 0] = $1; + HEAP8[$9 + 14 | 0] = $1 >>> 8; + HEAP8[$9 + 15 | 0] = $1 >>> 16; + HEAP8[$9 + 16 | 0] = $1 >>> 24; + HEAP8[$9 + 17 | 0] = $2; + HEAP8[$9 + 18 | 0] = $2 >>> 8; + HEAP8[$9 + 19 | 0] = $2 >>> 16; + HEAP8[$9 + 20 | 0] = $2 >>> 24; + HEAP32[$9 + 68 >> 2] = 51; + HEAP32[$9 + 72 >> 2] = 1; + HEAP32[$9 + 64 >> 2] = $9; + HEAP32[$0 + 388 >> 2] = 0; + break label$5; + } + HEAP32[$9 + 68 >> 2] = $2; + HEAP32[$9 + 64 >> 2] = $1; + } + if ($5) { + HEAP32[$9 + 76 >> 2] = 1 + } + $1 = $0 + 8 | 0; + if (ogg_stream_packetin($1, $9 - -64 | 0)) { + break label$4 + } + $2 = $0 + 368 | 0; + if (!$3) { + while (1) { + if (!ogg_stream_flush_i($1, $2, 1)) { + break label$2 + } + if (FUNCTION_TABLE[$6]($7, HEAP32[$0 + 368 >> 2], HEAP32[$0 + 372 >> 2], 0, $4, $8)) { + break label$4 + } + if (!FUNCTION_TABLE[$6]($7, HEAP32[$0 + 376 >> 2], HEAP32[$0 + 380 >> 2], 0, $4, $8)) { + continue + } + break label$4; + } + } + while (1) { + if (!ogg_stream_pageout($1, $2)) { + break label$2 + } + if (FUNCTION_TABLE[$6]($7, HEAP32[$0 + 368 >> 2], HEAP32[$0 + 372 >> 2], 0, $4, $8)) { + break label$4 + } + if (!FUNCTION_TABLE[$6]($7, HEAP32[$0 + 376 >> 2], HEAP32[$0 + 380 >> 2], 0, $4, $8)) { + continue + } + break; + }; + } + $6 = 1; + break label$1; + } + $6 = 1; + if ($3 | $4 | ($2 | 0) != 4 | (HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24)) != (HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24))) { + break label$1 + } + HEAP32[$0 + 384 >> 2] = 1; + $11 = $3; + } + $1 = $0; + $3 = $1; + $2 = HEAP32[$1 + 396 >> 2]; + $0 = $11 + HEAP32[$1 + 392 >> 2] | 0; + if ($0 >>> 0 < $11 >>> 0) { + $2 = $2 + 1 | 0 + } + HEAP32[$3 + 392 >> 2] = $0; + HEAP32[$1 + 396 >> 2] = $2; + $6 = 0; + } + global$0 = $9 + 96 | 0; + return $6; + } + + function simple_ogg_page__init($0) { + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + } + + function simple_ogg_page__clear($0) { + var $1 = 0; + $1 = HEAP32[$0 >> 2]; + if ($1) { + dlfree($1) + } + $1 = HEAP32[$0 + 8 >> 2]; + if ($1) { + dlfree($1) + } + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 0; + HEAP32[$0 + 12 >> 2] = 0; + } + + function simple_ogg_page__get_at($0, $1, $2, $3, $4, $5, $6) { + var $7 = 0, $8 = 0, $9 = 0; + $7 = global$0 - 16 | 0; + global$0 = $7; + label$1 : { + if (!$4) { + break label$1 + } + label$2 : { + switch (FUNCTION_TABLE[$4]($0, $1, $2, $6) | 0) { + case 1: + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$1; + case 0: + break label$2; + default: + break label$1; + }; + } + $4 = dlmalloc(282); + HEAP32[$3 >> 2] = $4; + if (!$4) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$1; + } + $8 = 27; + while (1) { + HEAP32[$7 + 12 >> 2] = $8; + $1 = 5; + label$6 : { + label$7 : { + switch (FUNCTION_TABLE[$5]($0, $4, $7 + 12 | 0, $6) | 0) { + case 1: + $1 = HEAP32[$7 + 12 >> 2]; + if ($1) { + break label$6 + } + $1 = 2; + default: + HEAP32[HEAP32[$0 >> 2] >> 2] = $1; + break label$1; + case 3: + break label$1; + case 0: + break label$7; + }; + } + $1 = HEAP32[$7 + 12 >> 2]; + } + $4 = $1 + $4 | 0; + $8 = $8 - $1 | 0; + if ($8) { + continue + } + break; + }; + $1 = HEAP32[$3 >> 2]; + HEAP32[$3 + 4 >> 2] = HEAPU8[$1 + 26 | 0] + 27; + label$10 : { + if (!(HEAP8[$1 + 5 | 0] & 1 | (HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24)) != 1399285583 | ((HEAPU8[$1 + 6 | 0] | HEAPU8[$1 + 7 | 0] << 8 | (HEAPU8[$1 + 8 | 0] << 16 | HEAPU8[$1 + 9 | 0] << 24)) != 0 | (HEAPU8[$1 + 10 | 0] | HEAPU8[$1 + 11 | 0] << 8 | (HEAPU8[$1 + 12 | 0] << 16 | HEAPU8[$1 + 13 | 0] << 24)) != 0))) { + $8 = HEAPU8[$1 + 26 | 0]; + if ($8) { + break label$10 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$1; + } + $4 = $1 + 27 | 0; + while (1) { + HEAP32[$7 + 12 >> 2] = $8; + $1 = 5; + label$13 : { + label$14 : { + switch (FUNCTION_TABLE[$5]($0, $4, $7 + 12 | 0, $6) | 0) { + case 1: + $1 = HEAP32[$7 + 12 >> 2]; + if ($1) { + break label$13 + } + $1 = 2; + default: + HEAP32[HEAP32[$0 >> 2] >> 2] = $1; + break label$1; + case 3: + break label$1; + case 0: + break label$14; + }; + } + $1 = HEAP32[$7 + 12 >> 2]; + } + $4 = $1 + $4 | 0; + $8 = $8 - $1 | 0; + if ($8) { + continue + } + break; + }; + $4 = 0; + $1 = HEAP32[$3 >> 2]; + $2 = HEAPU8[$1 + 26 | 0]; + label$17 : { + if (($2 | 0) != 1) { + $2 = $2 + -1 | 0; + while (1) { + if (HEAPU8[($1 + $4 | 0) + 27 | 0] != 255) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + break label$17; + } + $4 = $4 + 1 | 0; + if ($4 >>> 0 < $2 >>> 0) { + continue + } + break; + }; + } + $4 = HEAPU8[($1 + $4 | 0) + 27 | 0] + Math_imul($4, 255) | 0; + HEAP32[$3 + 12 >> 2] = $4; + $8 = dlmalloc($4 ? $4 : 1); + HEAP32[$3 + 8 >> 2] = $8; + if (!$8) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + break label$17; + } + $2 = $7; + if ($4) { + while (1) { + HEAP32[$7 + 12 >> 2] = $4; + $1 = 5; + label$24 : { + label$25 : { + switch (FUNCTION_TABLE[$5]($0, $8, $7 + 12 | 0, $6) | 0) { + case 1: + $1 = HEAP32[$7 + 12 >> 2]; + if ($1) { + break label$24 + } + $1 = 2; + default: + HEAP32[HEAP32[$0 >> 2] >> 2] = $1; + break label$17; + case 3: + break label$17; + case 0: + break label$25; + }; + } + $1 = HEAP32[$7 + 12 >> 2]; + } + $8 = $1 + $8 | 0; + $4 = $4 - $1 | 0; + if ($4) { + continue + } + break; + }; + $1 = HEAP32[$3 >> 2]; + } + HEAP32[$2 + 12 >> 2] = HEAPU8[$1 + 22 | 0] | HEAPU8[$1 + 23 | 0] << 8 | (HEAPU8[$1 + 24 | 0] << 16 | HEAPU8[$1 + 25 | 0] << 24); + ogg_page_checksum_set($3); + $1 = HEAP32[$3 >> 2]; + if (HEAP32[$7 + 12 >> 2] == (HEAPU8[$1 + 22 | 0] | HEAPU8[$1 + 23 | 0] << 8 | (HEAPU8[$1 + 24 | 0] << 16 | HEAPU8[$1 + 25 | 0] << 24))) { + $9 = 1; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + } + } + global$0 = $7 + 16 | 0; + return $9; + } + + function simple_ogg_page__set_at($0, $1, $2, $3, $4, $5, $6) { + folding_inner0 : { + label$1 : { + if (!$4) { + break label$1 + } + label$2 : { + switch (FUNCTION_TABLE[$4]($0, $1, $2, $6) | 0) { + case 1: + break folding_inner0; + case 0: + break label$2; + default: + break label$1; + }; + } + ogg_page_checksum_set($3); + if (FUNCTION_TABLE[$5]($0, HEAP32[$3 >> 2], HEAP32[$3 + 4 >> 2], 0, 0, $6)) { + break folding_inner0 + } + if (!FUNCTION_TABLE[$5]($0, HEAP32[$3 + 8 >> 2], HEAP32[$3 + 12 >> 2], 0, 0, $6)) { + return 1 + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + } + return 0; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + return 0; + } + + function __emscripten_stdout_close($0) { + $0 = $0 | 0; + return 0; + } + + function __emscripten_stdout_seek($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + i64toi32_i32$HIGH_BITS = 0; + return 0; + } + + function strcmp($0, $1) { + var $2 = 0, $3 = 0; + $2 = HEAPU8[$0 | 0]; + $3 = HEAPU8[$1 | 0]; + label$1 : { + if (!$2 | ($2 | 0) != ($3 | 0)) { + break label$1 + } + while (1) { + $3 = HEAPU8[$1 + 1 | 0]; + $2 = HEAPU8[$0 + 1 | 0]; + if (!$2) { + break label$1 + } + $1 = $1 + 1 | 0; + $0 = $0 + 1 | 0; + if (($2 | 0) == ($3 | 0)) { + continue + } + break; + }; + } + return $2 - $3 | 0; + } + + function __cos($0, $1) { + var $2 = 0.0, $3 = 0.0, $4 = 0.0, $5 = 0.0; + $2 = $0 * $0; + $3 = $2 * .5; + $4 = 1.0 - $3; + $5 = 1.0 - $4 - $3; + $3 = $2 * $2; + return $4 + ($5 + ($2 * ($2 * ($2 * ($2 * 2.480158728947673e-05 + -.001388888888887411) + .0416666666666666) + $3 * $3 * ($2 * ($2 * -1.1359647557788195e-11 + 2.087572321298175e-09) + -2.7557314351390663e-07)) - $0 * $1)); + } + + function scalbn($0, $1) { + label$1 : { + if (($1 | 0) >= 1024) { + $0 = $0 * 8988465674311579538646525.0e283; + if (($1 | 0) < 2047) { + $1 = $1 + -1023 | 0; + break label$1; + } + $0 = $0 * 8988465674311579538646525.0e283; + $1 = (($1 | 0) < 3069 ? $1 : 3069) + -2046 | 0; + break label$1; + } + if (($1 | 0) > -1023) { + break label$1 + } + $0 = $0 * 2.2250738585072014e-308; + if (($1 | 0) > -2045) { + $1 = $1 + 1022 | 0; + break label$1; + } + $0 = $0 * 2.2250738585072014e-308; + $1 = (($1 | 0) > -3066 ? $1 : -3066) + 2044 | 0; + } + wasm2js_scratch_store_i32(0, 0); + wasm2js_scratch_store_i32(1, $1 + 1023 << 20); + return $0 * +wasm2js_scratch_load_f64(); + } + + function __rem_pio2_large($0, $1, $2, $3) { + var $4 = 0.0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0.0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0; + $7 = global$0 - 560 | 0; + global$0 = $7; + $5 = ($2 + -3 | 0) / 24 | 0; + $16 = ($5 | 0) > 0 ? $5 : 0; + $10 = $2 + Math_imul($16, -24) | 0; + $12 = HEAP32[1901]; + $9 = $3 + -1 | 0; + if (($12 + $9 | 0) >= 0) { + $5 = $3 + $12 | 0; + $2 = $16 - $9 | 0; + while (1) { + HEAPF64[($7 + 320 | 0) + ($6 << 3) >> 3] = ($2 | 0) < 0 ? 0.0 : +HEAP32[($2 << 2) + 7616 >> 2]; + $2 = $2 + 1 | 0; + $6 = $6 + 1 | 0; + if (($5 | 0) != ($6 | 0)) { + continue + } + break; + }; + } + $13 = $10 + -24 | 0; + $5 = 0; + $6 = ($12 | 0) > 0 ? $12 : 0; + $11 = ($3 | 0) < 1; + while (1) { + label$6 : { + if ($11) { + $4 = 0.0; + break label$6; + } + $8 = $5 + $9 | 0; + $2 = 0; + $4 = 0.0; + while (1) { + $4 = $4 + HEAPF64[($2 << 3) + $0 >> 3] * HEAPF64[($7 + 320 | 0) + ($8 - $2 << 3) >> 3]; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + HEAPF64[($5 << 3) + $7 >> 3] = $4; + $2 = ($5 | 0) == ($6 | 0); + $5 = $5 + 1 | 0; + if (!$2) { + continue + } + break; + }; + $20 = 47 - $10 | 0; + $17 = 48 - $10 | 0; + $21 = $10 + -25 | 0; + $5 = $12; + label$9 : { + while (1) { + $4 = HEAPF64[($5 << 3) + $7 >> 3]; + $2 = 0; + $6 = $5; + $9 = ($5 | 0) < 1; + if (!$9) { + while (1) { + $11 = ($7 + 480 | 0) + ($2 << 2) | 0; + $14 = $4; + $4 = $4 * 5.9604644775390625e-08; + label$14 : { + if (Math_abs($4) < 2147483648.0) { + $8 = ~~$4; + break label$14; + } + $8 = -2147483648; + } + $4 = +($8 | 0); + $14 = $14 + $4 * -16777216.0; + label$13 : { + if (Math_abs($14) < 2147483648.0) { + $8 = ~~$14; + break label$13; + } + $8 = -2147483648; + } + HEAP32[$11 >> 2] = $8; + $6 = $6 + -1 | 0; + $4 = HEAPF64[($6 << 3) + $7 >> 3] + $4; + $2 = $2 + 1 | 0; + if (($5 | 0) != ($2 | 0)) { + continue + } + break; + } + } + $4 = scalbn($4, $13); + $4 = $4 + Math_floor($4 * .125) * -8.0; + label$17 : { + if (Math_abs($4) < 2147483648.0) { + $11 = ~~$4; + break label$17; + } + $11 = -2147483648; + } + $4 = $4 - +($11 | 0); + label$19 : { + label$20 : { + label$21 : { + $18 = ($13 | 0) < 1; + label$22 : { + if (!$18) { + $6 = (($5 << 2) + $7 | 0) + 476 | 0; + $8 = HEAP32[$6 >> 2]; + $2 = $8 >> $17; + $15 = $6; + $6 = $8 - ($2 << $17) | 0; + HEAP32[$15 >> 2] = $6; + $11 = $2 + $11 | 0; + $8 = $6 >> $20; + break label$22; + } + if ($13) { + break label$21 + } + $8 = HEAP32[(($5 << 2) + $7 | 0) + 476 >> 2] >> 23; + } + if (($8 | 0) < 1) { + break label$19 + } + break label$20; + } + $8 = 2; + if (!!($4 >= .5)) { + break label$20 + } + $8 = 0; + break label$19; + } + $2 = 0; + $6 = 0; + if (!$9) { + while (1) { + $15 = ($7 + 480 | 0) + ($2 << 2) | 0; + $19 = HEAP32[$15 >> 2]; + $9 = 16777215; + label$26 : { + label$27 : { + if ($6) { + break label$27 + } + $9 = 16777216; + if ($19) { + break label$27 + } + $6 = 0; + break label$26; + } + HEAP32[$15 >> 2] = $9 - $19; + $6 = 1; + } + $2 = $2 + 1 | 0; + if (($5 | 0) != ($2 | 0)) { + continue + } + break; + } + } + label$28 : { + if ($18) { + break label$28 + } + label$29 : { + switch ($21 | 0) { + case 0: + $2 = (($5 << 2) + $7 | 0) + 476 | 0; + HEAP32[$2 >> 2] = HEAP32[$2 >> 2] & 8388607; + break label$28; + case 1: + break label$29; + default: + break label$28; + }; + } + $2 = (($5 << 2) + $7 | 0) + 476 | 0; + HEAP32[$2 >> 2] = HEAP32[$2 >> 2] & 4194303; + } + $11 = $11 + 1 | 0; + if (($8 | 0) != 2) { + break label$19 + } + $4 = 1.0 - $4; + $8 = 2; + if (!$6) { + break label$19 + } + $4 = $4 - scalbn(1.0, $13); + } + if ($4 == 0.0) { + $6 = 0; + label$32 : { + $2 = $5; + if (($2 | 0) <= ($12 | 0)) { + break label$32 + } + while (1) { + $2 = $2 + -1 | 0; + $6 = HEAP32[($7 + 480 | 0) + ($2 << 2) >> 2] | $6; + if (($2 | 0) > ($12 | 0)) { + continue + } + break; + }; + if (!$6) { + break label$32 + } + $10 = $13; + while (1) { + $10 = $10 + -24 | 0; + $5 = $5 + -1 | 0; + if (!HEAP32[($7 + 480 | 0) + ($5 << 2) >> 2]) { + continue + } + break; + }; + break label$9; + } + $2 = 1; + while (1) { + $6 = $2; + $2 = $2 + 1 | 0; + if (!HEAP32[($7 + 480 | 0) + ($12 - $6 << 2) >> 2]) { + continue + } + break; + }; + $6 = $5 + $6 | 0; + while (1) { + $9 = $3 + $5 | 0; + $5 = $5 + 1 | 0; + HEAPF64[($7 + 320 | 0) + ($9 << 3) >> 3] = HEAP32[($16 + $5 << 2) + 7616 >> 2]; + $2 = 0; + $4 = 0.0; + if (($3 | 0) >= 1) { + while (1) { + $4 = $4 + HEAPF64[($2 << 3) + $0 >> 3] * HEAPF64[($7 + 320 | 0) + ($9 - $2 << 3) >> 3]; + $2 = $2 + 1 | 0; + if (($3 | 0) != ($2 | 0)) { + continue + } + break; + } + } + HEAPF64[($5 << 3) + $7 >> 3] = $4; + if (($5 | 0) < ($6 | 0)) { + continue + } + break; + }; + $5 = $6; + continue; + } + break; + }; + $4 = scalbn($4, 0 - $13 | 0); + label$39 : { + if (!!($4 >= 16777216.0)) { + $3 = ($7 + 480 | 0) + ($5 << 2) | 0; + $14 = $4; + $4 = $4 * 5.9604644775390625e-08; + label$42 : { + if (Math_abs($4) < 2147483648.0) { + $2 = ~~$4; + break label$42; + } + $2 = -2147483648; + } + $4 = $14 + +($2 | 0) * -16777216.0; + label$41 : { + if (Math_abs($4) < 2147483648.0) { + $0 = ~~$4; + break label$41; + } + $0 = -2147483648; + } + HEAP32[$3 >> 2] = $0; + $5 = $5 + 1 | 0; + break label$39; + } + $2 = Math_abs($4) < 2147483648.0 ? ~~$4 : -2147483648; + $10 = $13; + } + HEAP32[($7 + 480 | 0) + ($5 << 2) >> 2] = $2; + } + $4 = scalbn(1.0, $10); + label$47 : { + if (($5 | 0) <= -1) { + break label$47 + } + $2 = $5; + while (1) { + HEAPF64[($2 << 3) + $7 >> 3] = $4 * +HEAP32[($7 + 480 | 0) + ($2 << 2) >> 2]; + $4 = $4 * 5.9604644775390625e-08; + $0 = ($2 | 0) > 0; + $2 = $2 + -1 | 0; + if ($0) { + continue + } + break; + }; + $9 = 0; + if (($5 | 0) < 0) { + break label$47 + } + $0 = ($12 | 0) > 0 ? $12 : 0; + $6 = $5; + while (1) { + $3 = $0 >>> 0 < $9 >>> 0 ? $0 : $9; + $10 = $5 - $6 | 0; + $2 = 0; + $4 = 0.0; + while (1) { + $4 = $4 + HEAPF64[($2 << 3) + 10384 >> 3] * HEAPF64[($2 + $6 << 3) + $7 >> 3]; + $13 = ($2 | 0) != ($3 | 0); + $2 = $2 + 1 | 0; + if ($13) { + continue + } + break; + }; + HEAPF64[($7 + 160 | 0) + ($10 << 3) >> 3] = $4; + $6 = $6 + -1 | 0; + $2 = ($5 | 0) != ($9 | 0); + $9 = $9 + 1 | 0; + if ($2) { + continue + } + break; + }; + } + $4 = 0.0; + if (($5 | 0) >= 0) { + $2 = $5; + while (1) { + $4 = $4 + HEAPF64[($7 + 160 | 0) + ($2 << 3) >> 3]; + $0 = ($2 | 0) > 0; + $2 = $2 + -1 | 0; + if ($0) { + continue + } + break; + }; + } + HEAPF64[$1 >> 3] = $8 ? -$4 : $4; + $4 = HEAPF64[$7 + 160 >> 3] - $4; + $2 = 1; + if (($5 | 0) >= 1) { + while (1) { + $4 = $4 + HEAPF64[($7 + 160 | 0) + ($2 << 3) >> 3]; + $0 = ($2 | 0) != ($5 | 0); + $2 = $2 + 1 | 0; + if ($0) { + continue + } + break; + } + } + HEAPF64[$1 + 8 >> 3] = $8 ? -$4 : $4; + global$0 = $7 + 560 | 0; + return $11 & 7; + } + + function __rem_pio2($0, $1) { + var $2 = 0.0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0.0, $9 = 0.0, $10 = 0; + $6 = global$0 - 48 | 0; + global$0 = $6; + wasm2js_scratch_store_f64(+$0); + $5 = wasm2js_scratch_load_i32(1) | 0; + $3 = wasm2js_scratch_load_i32(0) | 0; + label$1 : { + label$2 : { + $4 = $5; + $5 = $4; + $7 = $4 & 2147483647; + label$3 : { + if ($7 >>> 0 <= 1074752122) { + if (($5 & 1048575) == 598523) { + break label$3 + } + if ($7 >>> 0 <= 1073928572) { + if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { + $0 = $0 + -1.5707963267341256; + $2 = $0 + -6.077100506506192e-11; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + -6.077100506506192e-11; + $3 = 1; + break label$1; + } + $0 = $0 + 1.5707963267341256; + $2 = $0 + 6.077100506506192e-11; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + 6.077100506506192e-11; + $3 = -1; + break label$1; + } + if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { + $0 = $0 + -3.1415926534682512; + $2 = $0 + -1.2154201013012384e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + -1.2154201013012384e-10; + $3 = 2; + break label$1; + } + $0 = $0 + 3.1415926534682512; + $2 = $0 + 1.2154201013012384e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + 1.2154201013012384e-10; + $3 = -2; + break label$1; + } + if ($7 >>> 0 <= 1075594811) { + if ($7 >>> 0 <= 1075183036) { + if (($7 | 0) == 1074977148) { + break label$3 + } + if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { + $0 = $0 + -4.712388980202377; + $2 = $0 + -1.8231301519518578e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + -1.8231301519518578e-10; + $3 = 3; + break label$1; + } + $0 = $0 + 4.712388980202377; + $2 = $0 + 1.8231301519518578e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + 1.8231301519518578e-10; + $3 = -3; + break label$1; + } + if (($7 | 0) == 1075388923) { + break label$3 + } + if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { + $0 = $0 + -6.2831853069365025; + $2 = $0 + -2.430840202602477e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + -2.430840202602477e-10; + $3 = 4; + break label$1; + } + $0 = $0 + 6.2831853069365025; + $2 = $0 + 2.430840202602477e-10; + HEAPF64[$1 >> 3] = $2; + HEAPF64[$1 + 8 >> 3] = $0 - $2 + 2.430840202602477e-10; + $3 = -4; + break label$1; + } + if ($7 >>> 0 > 1094263290) { + break label$2 + } + } + $9 = $0 * .6366197723675814 + 6755399441055744.0 + -6755399441055744.0; + $2 = $0 + $9 * -1.5707963267341256; + $8 = $9 * 6.077100506506192e-11; + $0 = $2 - $8; + HEAPF64[$1 >> 3] = $0; + wasm2js_scratch_store_f64(+$0); + $3 = wasm2js_scratch_load_i32(1) | 0; + wasm2js_scratch_load_i32(0) | 0; + $4 = $7 >>> 20 | 0; + $5 = ($4 - ($3 >>> 20 & 2047) | 0) < 17; + if (Math_abs($9) < 2147483648.0) { + $3 = ~~$9 + } else { + $3 = -2147483648 + } + label$14 : { + if ($5) { + break label$14 + } + $8 = $2; + $0 = $9 * 6.077100506303966e-11; + $2 = $2 - $0; + $8 = $9 * 2.0222662487959506e-21 - ($8 - $2 - $0); + $0 = $2 - $8; + HEAPF64[$1 >> 3] = $0; + $5 = $4; + wasm2js_scratch_store_f64(+$0); + $4 = wasm2js_scratch_load_i32(1) | 0; + wasm2js_scratch_load_i32(0) | 0; + if (($5 - ($4 >>> 20 & 2047) | 0) < 50) { + break label$14 + } + $8 = $2; + $0 = $9 * 2.0222662487111665e-21; + $2 = $2 - $0; + $8 = $9 * 8.4784276603689e-32 - ($8 - $2 - $0); + $0 = $2 - $8; + HEAPF64[$1 >> 3] = $0; + } + HEAPF64[$1 + 8 >> 3] = $2 - $0 - $8; + break label$1; + } + if ($7 >>> 0 >= 2146435072) { + $0 = $0 - $0; + HEAPF64[$1 >> 3] = $0; + HEAPF64[$1 + 8 >> 3] = $0; + $3 = 0; + break label$1; + } + wasm2js_scratch_store_i32(0, $3 | 0); + wasm2js_scratch_store_i32(1, $4 & 1048575 | 1096810496); + $0 = +wasm2js_scratch_load_f64(); + $3 = 0; + $5 = 1; + while (1) { + $10 = ($6 + 16 | 0) + ($3 << 3) | 0; + if (Math_abs($0) < 2147483648.0) { + $3 = ~~$0 + } else { + $3 = -2147483648 + } + $2 = +($3 | 0); + HEAPF64[$10 >> 3] = $2; + $0 = ($0 - $2) * 16777216.0; + $3 = 1; + $10 = $5 & 1; + $5 = 0; + if ($10) { + continue + } + break; + }; + HEAPF64[$6 + 32 >> 3] = $0; + label$20 : { + if ($0 != 0.0) { + $3 = 2; + break label$20; + } + $5 = 1; + while (1) { + $3 = $5; + $5 = $3 + -1 | 0; + if (HEAPF64[($6 + 16 | 0) + ($3 << 3) >> 3] == 0.0) { + continue + } + break; + }; + } + $3 = __rem_pio2_large($6 + 16 | 0, $6, ($7 >>> 20 | 0) + -1046 | 0, $3 + 1 | 0); + $0 = HEAPF64[$6 >> 3]; + if (($4 | 0) < -1 ? 1 : ($4 | 0) <= -1 ? 1 : 0) { + HEAPF64[$1 >> 3] = -$0; + HEAPF64[$1 + 8 >> 3] = -HEAPF64[$6 + 8 >> 3]; + $3 = 0 - $3 | 0; + break label$1; + } + HEAPF64[$1 >> 3] = $0; + $4 = HEAP32[$6 + 12 >> 2]; + HEAP32[$1 + 8 >> 2] = HEAP32[$6 + 8 >> 2]; + HEAP32[$1 + 12 >> 2] = $4; + } + global$0 = $6 + 48 | 0; + return $3; + } + + function __sin($0, $1) { + var $2 = 0.0, $3 = 0.0; + $2 = $0 * $0; + $3 = $0; + $0 = $2 * $0; + return $3 - ($2 * ($1 * .5 - $0 * ($2 * ($2 * $2) * ($2 * 1.58969099521155e-10 + -2.5050760253406863e-08) + ($2 * ($2 * 2.7557313707070068e-06 + -1.984126982985795e-04) + .00833333333332249))) - $1 + $0 * .16666666666666632); + } + + function cos($0) { + var $1 = 0, $2 = 0.0, $3 = 0; + $1 = global$0 - 16 | 0; + global$0 = $1; + wasm2js_scratch_store_f64(+$0); + $3 = wasm2js_scratch_load_i32(1) | 0; + wasm2js_scratch_load_i32(0) | 0; + $3 = $3 & 2147483647; + label$1 : { + if ($3 >>> 0 <= 1072243195) { + $2 = 1.0; + if ($3 >>> 0 < 1044816030) { + break label$1 + } + $2 = __cos($0, 0.0); + break label$1; + } + $2 = $0 - $0; + if ($3 >>> 0 >= 2146435072) { + break label$1 + } + label$3 : { + switch (__rem_pio2($0, $1) & 3) { + case 0: + $2 = __cos(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); + break label$1; + case 1: + $2 = -__sin(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); + break label$1; + case 2: + $2 = -__cos(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); + break label$1; + default: + break label$3; + }; + } + $2 = __sin(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); + } + $0 = $2; + global$0 = $1 + 16 | 0; + return $0; + } + + function exp($0) { + var $1 = 0, $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0, $6 = 0.0, $7 = 0; + wasm2js_scratch_store_f64(+$0); + $3 = wasm2js_scratch_load_i32(1) | 0; + $7 = wasm2js_scratch_load_i32(0) | 0; + $5 = $3 >>> 31 | 0; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + $6 = $0; + label$5 : { + label$6 : { + $1 = $3; + $3 = $1 & 2147483647; + label$7 : { + if ($3 >>> 0 >= 1082532651) { + $1 = $1 & 2147483647; + if (($1 | 0) == 2146435072 & $7 >>> 0 > 0 | $1 >>> 0 > 2146435072) { + return $0 + } + if (!!($0 > 709.782712893384)) { + return $0 * 8988465674311579538646525.0e283 + } + if (!($0 < -708.3964185322641)) { + break label$7 + } + if (!($0 < -745.1332191019411)) { + break label$7 + } + break label$2; + } + if ($3 >>> 0 < 1071001155) { + break label$4 + } + if ($3 >>> 0 < 1072734898) { + break label$6 + } + } + $0 = $0 * 1.4426950408889634 + HEAPF64[($5 << 3) + 10448 >> 3]; + if (Math_abs($0) < 2147483648.0) { + $1 = ~~$0; + break label$5; + } + $1 = -2147483648; + break label$5; + } + $1 = ($5 ^ 1) - $5 | 0; + } + $2 = +($1 | 0); + $0 = $6 + $2 * -.6931471803691238; + $4 = $2 * 1.9082149292705877e-10; + $2 = $0 - $4; + break label$3; + } + if ($3 >>> 0 <= 1043333120) { + break label$1 + } + $1 = 0; + $2 = $0; + } + $6 = $0; + $0 = $2 * $2; + $0 = $2 - $0 * ($0 * ($0 * ($0 * ($0 * 4.1381367970572385e-08 + -1.6533902205465252e-06) + 6.613756321437934e-05) + -2.7777777777015593e-03) + .16666666666666602); + $4 = $6 + ($2 * $0 / (2.0 - $0) - $4) + 1.0; + if (!$1) { + break label$2 + } + $4 = scalbn($4, $1); + } + return $4; + } + return $0 + 1.0; + } + + function FLAC__window_bartlett($0, $1) { + var $2 = 0, $3 = Math_fround(0), $4 = 0, $5 = Math_fround(0), $6 = 0, $7 = 0, $8 = 0; + $7 = $1 + -1 | 0; + label$1 : { + if ($1 & 1) { + $4 = ($7 | 0) / 2 | 0; + if (($1 | 0) >= 0) { + $8 = ($4 | 0) > 0 ? $4 : 0; + $6 = $8 + 1 | 0; + $5 = Math_fround($7 | 0); + while (1) { + $3 = Math_fround($2 | 0); + HEAPF32[($2 << 2) + $0 >> 2] = Math_fround($3 + $3) / $5; + $4 = ($2 | 0) == ($8 | 0); + $2 = $2 + 1 | 0; + if (!$4) { + continue + } + break; + }; + } + if (($6 | 0) >= ($1 | 0)) { + break label$1 + } + $5 = Math_fround($7 | 0); + while (1) { + $3 = Math_fround($6 | 0); + HEAPF32[($6 << 2) + $0 >> 2] = Math_fround(2.0) - Math_fround(Math_fround($3 + $3) / $5); + $6 = $6 + 1 | 0; + if (($6 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + $4 = ($1 | 0) / 2 | 0; + if (($1 | 0) >= 2) { + $5 = Math_fround($7 | 0); + while (1) { + $3 = Math_fround($2 | 0); + HEAPF32[($2 << 2) + $0 >> 2] = Math_fround($3 + $3) / $5; + $2 = $2 + 1 | 0; + if (($4 | 0) != ($2 | 0)) { + continue + } + break; + }; + $2 = $4; + } + if (($2 | 0) >= ($1 | 0)) { + break label$1 + } + $5 = Math_fround($7 | 0); + while (1) { + $3 = Math_fround($2 | 0); + HEAPF32[($2 << 2) + $0 >> 2] = Math_fround(2.0) - Math_fround(Math_fround($3 + $3) / $5); + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_bartlett_hann($0, $1) { + var $2 = 0, $3 = Math_fround(0), $4 = Math_fround(0), wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $4 = Math_fround($1 + -1 | 0); + while (1) { + $3 = Math_fround(Math_fround($2 | 0) / $4); + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(+Math_fround(Math_abs(Math_fround($3 + Math_fround(-.5)))) * -.47999998927116394 + .6200000047683716 + cos(+$3 * 6.283185307179586) * -.3799999952316284)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_blackman($0, $1) { + var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + $4 = +($2 | 0); + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .07999999821186066 + (cos($4 * 6.283185307179586 / $3) * -.5 + .41999998688697815))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_blackman_harris_4term_92db_sidelobe($0, $1) { + var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + $4 = +($2 | 0); + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .14127999544143677 + (cos($4 * 6.283185307179586 / $3) * -.488290011882782 + .35874998569488525) + cos($4 * 18.84955592153876 / $3) * -.011680000461637974)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_connes($0, $1) { + var $2 = 0.0, $3 = 0, $4 = 0.0; + if (($1 | 0) >= 1) { + $4 = +($1 + -1 | 0) * .5; + while (1) { + $2 = (+($3 | 0) - $4) / $4; + $2 = 1.0 - $2 * $2; + HEAPF32[($3 << 2) + $0 >> 2] = $2 * $2; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_flattop($0, $1) { + var $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0.0, $6 = 0.0, $7 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $2 = +($1 + -1 | 0); + while (1) { + $4 = +($3 | 0); + $5 = cos($4 * 12.566370614359172 / $2); + $6 = cos($4 * 6.283185307179586 / $2); + $7 = cos($4 * 18.84955592153876 / $2); + (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 25.132741228718345 / $2) * 6.9473679177463055e-03 + ($5 * .27726316452026367 + ($6 * -.4166315793991089 + .21557894349098206) + $7 * -.08357894420623779))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_gauss($0, $1, $2) { + var $3 = 0, $4 = 0.0, $5 = 0.0, $6 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $4 = +($1 + -1 | 0) * .5; + $6 = $4 * +$2; + while (1) { + $5 = (+($3 | 0) - $4) / $6; + (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(exp($5 * ($5 * -.5)))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_hamming($0, $1) { + var $2 = 0, $3 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos(+($2 | 0) * 6.283185307179586 / $3) * -.46000000834465027 + .5400000214576721)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_hann($0, $1) { + var $2 = 0, $3 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($2 | 0) * 6.283185307179586 / $3) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_kaiser_bessel($0, $1) { + var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + $4 = +($2 | 0); + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .09799999743700027 + (cos($4 * 6.283185307179586 / $3) * -.49799999594688416 + .4020000100135803) + cos($4 * 18.84955592153876 / $3) * -1.0000000474974513e-03)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_nuttall($0, $1) { + var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0); + while (1) { + $4 = +($2 | 0); + (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .13659949600696564 + (cos($4 * 6.283185307179586 / $3) * -.48917749524116516 + .36358189582824707) + cos($4 * 18.84955592153876 / $3) * -.010641099885106087)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_rectangle($0, $1) { + var $2 = 0; + if (($1 | 0) >= 1) { + while (1) { + HEAP32[($2 << 2) + $0 >> 2] = 1065353216; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + } + } + } + + function FLAC__window_triangle($0, $1) { + var $2 = 0, $3 = 0, $4 = Math_fround(0), $5 = 0, $6 = Math_fround(0), $7 = 0; + $3 = 1; + label$1 : { + if ($1 & 1) { + $2 = ($1 + 1 | 0) / 2 | 0; + if (($1 | 0) >= 1) { + $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); + $5 = ($2 | 0) > 1 ? $2 : 1; + $3 = $5 + 1 | 0; + $2 = 1; + while (1) { + $6 = Math_fround($2 | 0); + HEAPF32[(($2 << 2) + $0 | 0) + -4 >> 2] = Math_fround($6 + $6) / $4; + $7 = ($2 | 0) == ($5 | 0); + $2 = $2 + 1 | 0; + if (!$7) { + continue + } + break; + }; + } + if (($3 | 0) > ($1 | 0)) { + break label$1 + } + $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); + while (1) { + HEAPF32[(($3 << 2) + $0 | 0) + -4 >> 2] = Math_fround(($1 - $3 << 1) + 2 | 0) / $4; + $2 = ($1 | 0) == ($3 | 0); + $3 = $3 + 1 | 0; + if (!$2) { + continue + } + break; + }; + break label$1; + } + $2 = 1; + if (($1 | 0) >= 2) { + $5 = $1 >>> 1 | 0; + $2 = $5 + 1 | 0; + $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); + while (1) { + $6 = Math_fround($3 | 0); + HEAPF32[(($3 << 2) + $0 | 0) + -4 >> 2] = Math_fround($6 + $6) / $4; + $7 = ($3 | 0) == ($5 | 0); + $3 = $3 + 1 | 0; + if (!$7) { + continue + } + break; + }; + } + if (($2 | 0) > ($1 | 0)) { + break label$1 + } + $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); + while (1) { + HEAPF32[(($2 << 2) + $0 | 0) + -4 >> 2] = Math_fround(($1 - $2 << 1) + 2 | 0) / $4; + $3 = ($1 | 0) != ($2 | 0); + $2 = $2 + 1 | 0; + if ($3) { + continue + } + break; + }; + } + } + + function FLAC__window_tukey($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0.0, $6 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + label$1 : { + if (!!($2 <= Math_fround(0.0))) { + if (($1 | 0) < 1) { + break label$1 + } + while (1) { + HEAP32[($3 << 2) + $0 >> 2] = 1065353216; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + if (!!($2 >= Math_fround(1.0))) { + if (($1 | 0) < 1) { + break label$1 + } + $5 = +($1 + -1 | 0); + while (1) { + (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 | 0) * 6.283185307179586 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + break label$1; + } + $2 = Math_fround(Math_fround($2 * Math_fround(.5)) * Math_fround($1 | 0)); + label$6 : { + if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { + $4 = ~~$2; + break label$6; + } + $4 = -2147483648; + } + if (($1 | 0) >= 1) { + while (1) { + HEAP32[($3 << 2) + $0 >> 2] = 1065353216; + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + } + } + if (($4 | 0) < 2) { + break label$1 + } + $1 = $1 - $4 | 0; + $6 = $4 + -1 | 0; + $5 = +($6 | 0); + $3 = 0; + while (1) { + (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 | 0) * 3.141592653589793 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + (wasm2js_i32$0 = ($1 + $3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 + $6 | 0) * 3.141592653589793 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $3 = $3 + 1 | 0; + if (($4 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_partial_tukey($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = Math_fround(0), $12 = 0.0, $13 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + while (1) { + $11 = $2; + $2 = Math_fround(.05000000074505806); + if ($11 <= Math_fround(0.0)) { + continue + } + $2 = Math_fround(.949999988079071); + if ($11 >= Math_fround(1.0)) { + continue + } + break; + }; + $2 = Math_fround($1 | 0); + $3 = Math_fround($2 * $3); + label$2 : { + if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { + $6 = ~~$3; + break label$2; + } + $6 = -2147483648; + } + $3 = Math_fround($11 * Math_fround(.5)); + $2 = Math_fround($2 * $4); + label$5 : { + if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { + $10 = ~~$2; + break label$5; + } + $10 = -2147483648; + } + $2 = Math_fround($3 * Math_fround($10 - $6 | 0)); + label$4 : { + if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { + $7 = ~~$2; + break label$4; + } + $7 = -2147483648; + } + if (!(($6 | 0) < 1 | ($1 | 0) < 1)) { + $5 = $6 + -1 | 0; + $8 = $1 + -1 | 0; + $8 = $5 >>> 0 < $8 >>> 0 ? $5 : $8; + memset($0, ($8 << 2) + 4 | 0); + $5 = $8 + 1 | 0; + while (1) { + $13 = ($9 | 0) == ($8 | 0); + $9 = $9 + 1 | 0; + if (!$13) { + continue + } + break; + }; + } + $6 = $6 + $7 | 0; + label$10 : { + if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$10 + } + $12 = +($7 | 0); + $9 = 1; + while (1) { + (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($9 | 0) * 3.141592653589793 / $12) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($6 | 0)) { + break label$10 + } + $9 = $9 + 1 | 0; + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + $6 = $10 - $7 | 0; + label$12 : { + if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$12 + } + while (1) { + HEAP32[($5 << 2) + $0 >> 2] = 1065353216; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($6 | 0)) { + break label$12 + } + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + label$14 : { + if (($5 | 0) >= ($10 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$14 + } + $12 = +($7 | 0); + while (1) { + (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($7 | 0) * 3.141592653589793 / $12) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($10 | 0)) { + break label$14 + } + $7 = $7 + -1 | 0; + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + if (($5 | 0) < ($1 | 0)) { + memset(($5 << 2) + $0 | 0, $1 - $5 << 2) + } + } + + function FLAC__window_punchout_tukey($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0.0, $12 = Math_fround(0), $13 = 0, $14 = Math_fround(0), wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); + while (1) { + $12 = $2; + $2 = Math_fround(.05000000074505806); + if ($12 <= Math_fround(0.0)) { + continue + } + $2 = Math_fround(.949999988079071); + if ($12 >= Math_fround(1.0)) { + continue + } + break; + }; + $2 = Math_fround($12 * Math_fround(.5)); + $14 = $2; + $12 = Math_fround($1 | 0); + $3 = Math_fround($12 * $3); + label$3 : { + if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { + $10 = ~~$3; + break label$3; + } + $10 = -2147483648; + } + $3 = Math_fround($14 * Math_fround($10 | 0)); + label$2 : { + if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { + $6 = ~~$3; + break label$2; + } + $6 = -2147483648; + } + $8 = ($6 | 0) < 1; + $7 = $1; + $3 = Math_fround($12 * $4); + label$7 : { + if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { + $9 = ~~$3; + break label$7; + } + $9 = -2147483648; + } + $2 = Math_fround($2 * Math_fround($7 - $9 | 0)); + label$6 : { + if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { + $7 = ~~$2; + break label$6; + } + $7 = -2147483648; + } + if (!(($1 | 0) < 1 | $8)) { + $5 = $6 + -1 >>> 0 < $1 + -1 >>> 0 ? $6 : $1; + $11 = +($6 | 0); + $8 = 0; + $13 = 1; + while (1) { + (wasm2js_i32$0 = ($8 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($13 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $13 = $13 + 1 | 0; + $8 = $8 + 1 | 0; + if (($8 | 0) != ($5 | 0)) { + continue + } + break; + }; + } + $8 = $10 - $6 | 0; + label$12 : { + if (($5 | 0) >= ($8 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$12 + } + while (1) { + HEAP32[($5 << 2) + $0 >> 2] = 1065353216; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($8 | 0)) { + break label$12 + } + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + label$14 : { + if (($5 | 0) >= ($10 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$14 + } + $11 = +($6 | 0); + while (1) { + (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($6 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($10 | 0)) { + break label$14 + } + $6 = $6 + -1 | 0; + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + label$16 : { + if (($5 | 0) >= ($9 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$16 + } + $6 = $5 ^ -1; + $10 = $6 + $9 | 0; + $6 = $1 + $6 | 0; + memset(($5 << 2) + $0 | 0, (($10 >>> 0 < $6 >>> 0 ? $10 : $6) << 2) + 4 | 0); + while (1) { + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($9 | 0)) { + break label$16 + } + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + $9 = $7 + $9 | 0; + label$18 : { + if (($5 | 0) >= ($9 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$18 + } + $11 = +($7 | 0); + $6 = 1; + while (1) { + (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($6 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($9 | 0)) { + break label$18 + } + $6 = $6 + 1 | 0; + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + $6 = $1 - $7 | 0; + label$20 : { + if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { + break label$20 + } + while (1) { + HEAP32[($5 << 2) + $0 >> 2] = 1065353216; + $5 = $5 + 1 | 0; + if (($5 | 0) >= ($6 | 0)) { + break label$20 + } + if (($5 | 0) < ($1 | 0)) { + continue + } + break; + }; + } + if (($5 | 0) < ($1 | 0)) { + $11 = +($7 | 0); + while (1) { + (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($7 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; + $7 = $7 + -1 | 0; + $5 = $5 + 1 | 0; + if (($5 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__window_welch($0, $1) { + var $2 = 0, $3 = 0.0, $4 = 0.0; + if (($1 | 0) >= 1) { + $3 = +($1 + -1 | 0) * .5; + while (1) { + $4 = (+($2 | 0) - $3) / $3; + HEAPF32[($2 << 2) + $0 >> 2] = 1.0 - $4 * $4; + $2 = $2 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + } + + function FLAC__add_metadata_block($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0; + $3 = strlen(HEAP32[2720]); + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 4 >> 2], HEAP32[1391])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 >> 2], HEAP32[1392])) { + break label$1 + } + $2 = HEAP32[$0 + 8 >> 2]; + $2 = HEAP32[$0 >> 2] == 4 ? ($2 + $3 | 0) - HEAP32[$0 + 16 >> 2] | 0 : $2; + $4 = HEAP32[1393]; + if ($2 >>> $4) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, $2, $4)) { + break label$1 + } + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + label$8 : { + label$9 : { + switch (HEAP32[$0 >> 2]) { + case 3: + if (!HEAP32[$0 + 16 >> 2]) { + break label$3 + } + $4 = HEAP32[1367]; + $6 = HEAP32[1366]; + $7 = HEAP32[1365]; + $2 = 0; + break label$8; + case 0: + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 16 >> 2], HEAP32[1356])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 20 >> 2], HEAP32[1357])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 24 >> 2], HEAP32[1358])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 28 >> 2], HEAP32[1359])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 32 >> 2], HEAP32[1360])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 36 >> 2] + -1 | 0, HEAP32[1361])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 40 >> 2] + -1 | 0, HEAP32[1362])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$0 + 48 >> 2], HEAP32[$0 + 52 >> 2], HEAP32[1363])) { + break label$1 + } + if (FLAC__bitwriter_write_byte_block($1, $0 + 56 | 0, 16)) { + break label$3 + } + break label$1; + case 1: + if (FLAC__bitwriter_write_zeroes($1, HEAP32[$0 + 8 >> 2] << 3)) { + break label$3 + } + break label$1; + case 6: + break label$5; + case 5: + break label$6; + case 4: + break label$7; + case 2: + break label$9; + default: + break label$4; + }; + } + $2 = HEAP32[1364] >>> 3 | 0; + if (!FLAC__bitwriter_write_byte_block($1, $0 + 16 | 0, $2)) { + break label$1 + } + if (FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 20 >> 2], HEAP32[$0 + 8 >> 2] - $2 | 0)) { + break label$3 + } + break label$1; + } + while (1) { + $3 = Math_imul($2, 24); + $5 = $3 + HEAP32[$0 + 20 >> 2] | 0; + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$5 >> 2], HEAP32[$5 + 4 >> 2], $7)) { + break label$1 + } + $5 = $3 + HEAP32[$0 + 20 >> 2] | 0; + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$5 + 8 >> 2], HEAP32[$5 + 12 >> 2], $6)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[($3 + HEAP32[$0 + 20 >> 2] | 0) + 16 >> 2], $4)) { + break label$1 + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 < HEAPU32[$0 + 16 >> 2]) { + continue + } + break; + }; + break label$3; + } + if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, $3)) { + break label$1 + } + if (!FLAC__bitwriter_write_byte_block($1, HEAP32[2720], $3)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, HEAP32[$0 + 24 >> 2])) { + break label$1 + } + if (!HEAP32[$0 + 24 >> 2]) { + break label$3 + } + $2 = 0; + while (1) { + $3 = $2 << 3; + if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, HEAP32[$3 + HEAP32[$0 + 28 >> 2] >> 2])) { + break label$1 + } + $3 = $3 + HEAP32[$0 + 28 >> 2] | 0; + if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$3 + 4 >> 2], HEAP32[$3 >> 2])) { + break label$1 + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 < HEAPU32[$0 + 24 >> 2]) { + continue + } + break; + }; + break label$3; + } + if (!FLAC__bitwriter_write_byte_block($1, $0 + 16 | 0, HEAP32[1378] >>> 3 | 0)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$0 + 152 >> 2], HEAP32[$0 + 156 >> 2], HEAP32[1379])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 160 >> 2] != 0, HEAP32[1380])) { + break label$1 + } + if (!FLAC__bitwriter_write_zeroes($1, HEAP32[1381])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 164 >> 2], HEAP32[1382])) { + break label$1 + } + if (!HEAP32[$0 + 164 >> 2]) { + break label$3 + } + $6 = HEAP32[1373] >>> 3 | 0; + $7 = HEAP32[1370]; + $5 = HEAP32[1369]; + $9 = HEAP32[1368]; + $10 = HEAP32[1377]; + $11 = HEAP32[1376]; + $12 = HEAP32[1375]; + $13 = HEAP32[1374]; + $14 = HEAP32[1372]; + $15 = HEAP32[1371]; + $3 = 0; + while (1) { + $2 = HEAP32[$0 + 168 >> 2] + ($3 << 5) | 0; + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$2 >> 2], HEAP32[$2 + 4 >> 2], $15)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 8 | 0], $14)) { + break label$1 + } + if (!FLAC__bitwriter_write_byte_block($1, $2 + 9 | 0, $6)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP8[$2 + 22 | 0] & 1, $13)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 22 | 0] >>> 1 & 1, $12)) { + break label$1 + } + if (!FLAC__bitwriter_write_zeroes($1, $11)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 23 | 0], $10)) { + break label$1 + } + label$16 : { + $8 = $2 + 23 | 0; + if (!HEAPU8[$8 | 0]) { + break label$16 + } + $16 = $2 + 24 | 0; + $2 = 0; + while (1) { + $4 = HEAP32[$16 >> 2] + ($2 << 4) | 0; + if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$4 >> 2], HEAP32[$4 + 4 >> 2], $9)) { + return 0 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$4 + 8 | 0], $5)) { + return 0 + } + if (FLAC__bitwriter_write_zeroes($1, $7)) { + $2 = $2 + 1 | 0; + if ($2 >>> 0 >= HEAPU8[$8 | 0]) { + break label$16 + } + continue; + } + break; + }; + return 0; + } + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$0 + 164 >> 2]) { + continue + } + break; + }; + break label$3; + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 16 >> 2], HEAP32[1383])) { + break label$1 + } + $2 = strlen(HEAP32[$0 + 20 >> 2]); + if (!FLAC__bitwriter_write_raw_uint32($1, $2, HEAP32[1384])) { + break label$1 + } + if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 20 >> 2], $2)) { + break label$1 + } + $2 = strlen(HEAP32[$0 + 24 >> 2]); + if (!FLAC__bitwriter_write_raw_uint32($1, $2, HEAP32[1385])) { + break label$1 + } + if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 24 >> 2], $2)) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 28 >> 2], HEAP32[1386])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 32 >> 2], HEAP32[1387])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 36 >> 2], HEAP32[1388])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 40 >> 2], HEAP32[1389])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 44 >> 2], HEAP32[1390])) { + break label$1 + } + if (FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 48 >> 2], HEAP32[$0 + 44 >> 2])) { + break label$3 + } + break label$1; + } + if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 16 >> 2], HEAP32[$0 + 8 >> 2])) { + break label$1 + } + } + $17 = 1; + } + return $17; + } + + function FLAC__frame_add_header($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $5 = global$0 - 16 | 0; + global$0 = $5; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[1394], HEAP32[1395])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, 0, HEAP32[1396])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 20 >> 2] != 0, HEAP32[1397])) { + break label$1 + } + $8 = 16; + $9 = 1; + $3 = $1; + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + label$8 : { + label$9 : { + label$10 : { + label$11 : { + $2 = HEAP32[$0 >> 2]; + if (($2 | 0) <= 2047) { + if (($2 | 0) <= 575) { + $4 = 1; + if (($2 | 0) == 192) { + break label$3 + } + if (($2 | 0) == 256) { + break label$8 + } + if (($2 | 0) != 512) { + break label$4 + } + $4 = 9; + break label$3; + } + if (($2 | 0) == 576) { + break label$11 + } + if (($2 | 0) == 1024) { + break label$7 + } + if (($2 | 0) != 1152) { + break label$4 + } + $4 = 3; + break label$3; + } + if (($2 | 0) <= 4607) { + if (($2 | 0) == 2048) { + break label$6 + } + if (($2 | 0) == 2304) { + break label$10 + } + if (($2 | 0) != 4096) { + break label$4 + } + $4 = 12; + break label$3; + } + if (($2 | 0) <= 16383) { + if (($2 | 0) == 4608) { + break label$9 + } + if (($2 | 0) != 8192) { + break label$4 + } + $4 = 13; + break label$3; + } + if (($2 | 0) == 16384) { + break label$5 + } + if (($2 | 0) != 32768) { + break label$4 + } + $4 = 15; + break label$3; + } + $4 = 2; + break label$3; + } + $4 = 4; + break label$3; + } + $4 = 5; + break label$3; + } + $4 = 8; + break label$3; + } + $4 = 10; + break label$3; + } + $4 = 11; + break label$3; + } + $4 = 14; + break label$3; + } + $2 = $2 >>> 0 < 257; + $8 = $2 ? 8 : 16; + $9 = 0; + $4 = $2 ? 6 : 7; + } + if (!FLAC__bitwriter_write_raw_uint32($3, $4, HEAP32[1398])) { + break label$1 + } + label$16 : { + label$17 : { + label$18 : { + label$19 : { + label$20 : { + label$21 : { + label$22 : { + label$23 : { + $2 = HEAP32[$0 + 4 >> 2]; + if (($2 | 0) <= 44099) { + if (($2 | 0) <= 22049) { + if (($2 | 0) == 8e3) { + break label$23 + } + if (($2 | 0) != 16e3) { + break label$17 + } + $3 = 5; + break label$16; + } + if (($2 | 0) == 22050) { + break label$22 + } + if (($2 | 0) == 24e3) { + break label$21 + } + if (($2 | 0) != 32e3) { + break label$17 + } + $3 = 8; + break label$16; + } + if (($2 | 0) <= 95999) { + if (($2 | 0) == 44100) { + break label$20 + } + if (($2 | 0) == 48e3) { + break label$19 + } + $3 = 1; + if (($2 | 0) == 88200) { + break label$16 + } + break label$17; + } + if (($2 | 0) == 96e3) { + break label$18 + } + if (($2 | 0) != 192e3) { + if (($2 | 0) != 176400) { + break label$17 + } + $3 = 2; + break label$16; + } + $3 = 3; + break label$16; + } + $3 = 4; + break label$16; + } + $3 = 6; + break label$16; + } + $3 = 7; + break label$16; + } + $3 = 9; + break label$16; + } + $3 = 10; + break label$16; + } + $3 = 11; + break label$16; + } + $6 = ($2 >>> 0) % 1e3 | 0; + if ($2 >>> 0 <= 255e3) { + $3 = 12; + $7 = 12; + if (!$6) { + break label$16 + } + } + if (!(($2 >>> 0) % 10)) { + $3 = 14; + $7 = 14; + break label$16; + } + $3 = $2 >>> 0 < 65536 ? 13 : 0; + $7 = $3; + } + $6 = 0; + if (!FLAC__bitwriter_write_raw_uint32($1, $3, HEAP32[1399])) { + break label$1 + } + label$30 : { + label$31 : { + switch (HEAP32[$0 + 12 >> 2]) { + case 0: + $3 = HEAP32[$0 + 8 >> 2] + -1 | 0; + break label$30; + case 1: + $3 = 8; + break label$30; + case 2: + $3 = 9; + break label$30; + case 3: + break label$31; + default: + break label$30; + }; + } + $3 = 10; + } + if (!FLAC__bitwriter_write_raw_uint32($1, $3, HEAP32[1400])) { + break label$1 + } + $3 = $1; + $2 = __wasm_rotl_i32(HEAP32[$0 + 16 >> 2] + -8 | 0, 30); + if ($2 >>> 0 <= 4) { + $2 = HEAP32[($2 << 2) + 10464 >> 2] + } else { + $2 = 0 + } + if (!FLAC__bitwriter_write_raw_uint32($3, $2, HEAP32[1401])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_uint32($1, 0, HEAP32[1402])) { + break label$1 + } + label$37 : { + if (!HEAP32[$0 + 20 >> 2]) { + if (FLAC__bitwriter_write_utf8_uint32($1, HEAP32[$0 + 24 >> 2])) { + break label$37 + } + break label$1; + } + if (!FLAC__bitwriter_write_utf8_uint64($1, HEAP32[$0 + 24 >> 2], HEAP32[$0 + 28 >> 2])) { + break label$1 + } + } + if (!$9) { + if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 >> 2] + -1 | 0, $8)) { + break label$1 + } + } + label$40 : { + label$41 : { + switch ($7 + -12 | 0) { + case 0: + if (FLAC__bitwriter_write_raw_uint32($1, HEAPU32[$0 + 4 >> 2] / 1e3 | 0, 8)) { + break label$40 + } + break label$1; + case 1: + if (FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 4 >> 2], 16)) { + break label$40 + } + break label$1; + case 2: + break label$41; + default: + break label$40; + }; + } + if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU32[$0 + 4 >> 2] / 10 | 0, 16)) { + break label$1 + } + } + if (!FLAC__bitwriter_get_write_crc8($1, $5 + 15 | 0)) { + break label$1 + } + $6 = (FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$5 + 15 | 0], HEAP32[1403]) | 0) != 0; + } + global$0 = $5 + 16 | 0; + return $6; + } + + function FLAC__subframe_add_constant($0, $1, $2, $3) { + var $4 = 0; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($3, HEAP32[1417] | ($2 | 0) != 0, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { + break label$1 + } + if ($2) { + if (!FLAC__bitwriter_write_unary_unsigned($3, $2 + -1 | 0)) { + break label$1 + } + } + $4 = (FLAC__bitwriter_write_raw_int32($3, HEAP32[$0 >> 2], $1) | 0) != 0; + } + return $4; + } + + function FLAC__subframe_add_fixed($0, $1, $2, $3, $4) { + var $5 = 0; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[1419] | ($3 | 0) != 0 | HEAP32[$0 + 12 >> 2] << 1, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { + break label$1 + } + if ($3) { + if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { + break label$1 + } + } + label$3 : { + if (!HEAP32[$0 + 12 >> 2]) { + break label$3 + } + $3 = 0; + while (1) { + if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 16 >> 2], $2)) { + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { + continue + } + break label$3; + } + break; + }; + return 0; + } + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 >> 2], HEAP32[1405])) { + break label$1 + } + label$6 : { + if (HEAPU32[$0 >> 2] > 1) { + break label$6 + } + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 4 >> 2], HEAP32[1406])) { + break label$1 + } + $2 = HEAP32[$0 >> 2]; + if ($2 >>> 0 > 1) { + break label$6 + } + $3 = $1; + $1 = HEAP32[$0 + 8 >> 2]; + if (!add_residual_partitioned_rice_($4, HEAP32[$0 + 32 >> 2], $3, HEAP32[$0 + 12 >> 2], HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$0 + 4 >> 2], ($2 | 0) == 1)) { + break label$1 + } + } + $5 = 1; + } + return $5; + } + + function add_residual_partitioned_rice_($0, $1, $2, $3, $4, $5, $6, $7) { + var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; + $12 = HEAP32[($7 ? 5644 : 5640) >> 2]; + $9 = HEAP32[($7 ? 5632 : 5628) >> 2]; + label$1 : { + label$2 : { + if (!$6) { + if (!HEAP32[$5 >> 2]) { + if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$4 >> 2], $9)) { + break label$2 + } + if (!FLAC__bitwriter_write_rice_signed_block($0, $1, $2, HEAP32[$4 >> 2])) { + break label$2 + } + break label$1; + } + if (!FLAC__bitwriter_write_raw_uint32($0, $12, $9)) { + break label$2 + } + if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$5 >> 2], HEAP32[1409])) { + break label$2 + } + if (!$2) { + break label$1 + } + $7 = 0; + while (1) { + if (FLAC__bitwriter_write_raw_int32($0, HEAP32[($7 << 2) + $1 >> 2], HEAP32[$5 >> 2])) { + $7 = $7 + 1 | 0; + if (($7 | 0) != ($2 | 0)) { + continue + } + break label$1; + } + break; + }; + return 0; + } + $15 = $2 + $3 >>> $6 | 0; + $16 = HEAP32[1409]; + $2 = 0; + while (1) { + $7 = $2; + $13 = $15 - ($10 ? 0 : $3) | 0; + $2 = $7 + $13 | 0; + $14 = $10 << 2; + $8 = $14 + $5 | 0; + label$8 : { + if (!HEAP32[$8 >> 2]) { + $11 = 0; + $8 = $4 + $14 | 0; + if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$8 >> 2], $9)) { + break label$2 + } + if (FLAC__bitwriter_write_rice_signed_block($0, ($7 << 2) + $1 | 0, $13, HEAP32[$8 >> 2])) { + break label$8 + } + break label$2; + } + $11 = 0; + if (!FLAC__bitwriter_write_raw_uint32($0, $12, $9)) { + break label$2 + } + if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$8 >> 2], $16)) { + break label$2 + } + if ($7 >>> 0 >= $2 >>> 0) { + break label$8 + } + while (1) { + if (!FLAC__bitwriter_write_raw_int32($0, HEAP32[($7 << 2) + $1 >> 2], HEAP32[$8 >> 2])) { + break label$2 + } + $7 = $7 + 1 | 0; + if (($7 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + $11 = 1; + $10 = $10 + 1 | 0; + if (!($10 >>> $6)) { + continue + } + break; + }; + } + return $11; + } + return 1; + } + + function FLAC__subframe_add_lpc($0, $1, $2, $3, $4) { + var $5 = 0; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($4, (HEAP32[$0 + 12 >> 2] << 1) + -2 | (HEAP32[1420] | ($3 | 0) != 0), HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { + break label$1 + } + if ($3) { + if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { + break label$1 + } + } + label$3 : { + if (!HEAP32[$0 + 12 >> 2]) { + break label$3 + } + $3 = 0; + while (1) { + if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 152 >> 2], $2)) { + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { + continue + } + break label$3; + } + break; + }; + return 0; + } + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 16 >> 2] + -1 | 0, HEAP32[1412])) { + break label$1 + } + if (!FLAC__bitwriter_write_raw_int32($4, HEAP32[$0 + 20 >> 2], HEAP32[1413])) { + break label$1 + } + label$6 : { + if (!HEAP32[$0 + 12 >> 2]) { + break label$6 + } + $3 = 0; + while (1) { + if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 24 >> 2], HEAP32[$0 + 16 >> 2])) { + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { + continue + } + break label$6; + } + break; + }; + return 0; + } + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 >> 2], HEAP32[1405])) { + break label$1 + } + label$9 : { + if (HEAPU32[$0 >> 2] > 1) { + break label$9 + } + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 4 >> 2], HEAP32[1406])) { + break label$1 + } + $2 = HEAP32[$0 >> 2]; + if ($2 >>> 0 > 1) { + break label$9 + } + $3 = $1; + $1 = HEAP32[$0 + 8 >> 2]; + if (!add_residual_partitioned_rice_($4, HEAP32[$0 + 280 >> 2], $3, HEAP32[$0 + 12 >> 2], HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$0 + 4 >> 2], ($2 | 0) == 1)) { + break label$1 + } + } + $5 = 1; + } + return $5; + } + + function FLAC__subframe_add_verbatim($0, $1, $2, $3, $4) { + $0 = HEAP32[$0 >> 2]; + label$1 : { + if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[1418] | ($3 | 0) != 0, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { + break label$1 + } + if ($3) { + if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { + break label$1 + } + } + if (!$1) { + return 1 + } + $3 = 0; + label$4 : { + while (1) { + if (!FLAC__bitwriter_write_raw_int32($4, HEAP32[$0 + ($3 << 2) >> 2], $2)) { + break label$4 + } + $3 = $3 + 1 | 0; + if (($3 | 0) != ($1 | 0)) { + continue + } + break; + }; + return 1; + } + } + return 0; + } + + function strncmp($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0; + if (!$2) { + return 0 + } + $3 = HEAPU8[$0 | 0]; + label$2 : { + if (!$3) { + break label$2 + } + while (1) { + label$4 : { + $4 = HEAPU8[$1 | 0]; + if (($4 | 0) != ($3 | 0)) { + break label$4 + } + $2 = $2 + -1 | 0; + if (!$2 | !$4) { + break label$4 + } + $1 = $1 + 1 | 0; + $3 = HEAPU8[$0 + 1 | 0]; + $0 = $0 + 1 | 0; + if ($3) { + continue + } + break label$2; + } + break; + }; + $5 = $3; + } + return ($5 & 255) - HEAPU8[$1 | 0] | 0; + } + + function __uflow($0) { + var $1 = 0, $2 = 0; + $1 = global$0 - 16 | 0; + global$0 = $1; + $2 = -1; + label$1 : { + if (__toread($0)) { + break label$1 + } + if ((FUNCTION_TABLE[HEAP32[$0 + 32 >> 2]]($0, $1 + 15 | 0, 1) | 0) != 1) { + break label$1 + } + $2 = HEAPU8[$1 + 15 | 0]; + } + global$0 = $1 + 16 | 0; + return $2; + } + + function __shlim($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0; + HEAP32[$0 + 112 >> 2] = 0; + HEAP32[$0 + 116 >> 2] = 0; + $3 = HEAP32[$0 + 8 >> 2]; + $4 = HEAP32[$0 + 4 >> 2]; + $1 = $3 - $4 | 0; + $2 = $1 >> 31; + HEAP32[$0 + 120 >> 2] = $1; + HEAP32[$0 + 124 >> 2] = $2; + if (!((($2 | 0) < 0 ? 1 : ($2 | 0) <= 0 ? ($1 >>> 0 > 0 ? 0 : 1) : 0) | 1)) { + HEAP32[$0 + 104 >> 2] = $4; + return; + } + HEAP32[$0 + 104 >> 2] = $3; + } + + function __shgetc($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $2 = HEAP32[$0 + 116 >> 2]; + $3 = $2; + label$1 : { + $5 = HEAP32[$0 + 112 >> 2]; + label$2 : { + if ($2 | $5) { + $2 = HEAP32[$0 + 124 >> 2]; + if (($2 | 0) > ($3 | 0) ? 1 : ($2 | 0) >= ($3 | 0) ? (HEAPU32[$0 + 120 >> 2] < $5 >>> 0 ? 0 : 1) : 0) { + break label$2 + } + } + $5 = __uflow($0); + if (($5 | 0) > -1) { + break label$1 + } + } + HEAP32[$0 + 104 >> 2] = 0; + return -1; + } + $2 = HEAP32[$0 + 8 >> 2]; + $3 = HEAP32[$0 + 116 >> 2]; + $4 = $3; + label$4 : { + label$5 : { + $1 = HEAP32[$0 + 112 >> 2]; + if (!($3 | $1)) { + break label$5 + } + $3 = (HEAP32[$0 + 124 >> 2] ^ -1) + $4 | 0; + $4 = HEAP32[$0 + 120 >> 2] ^ -1; + $1 = $4 + $1 | 0; + if ($1 >>> 0 < $4 >>> 0) { + $3 = $3 + 1 | 0 + } + $4 = $1; + $1 = HEAP32[$0 + 4 >> 2]; + $6 = $2 - $1 | 0; + $7 = $4 >>> 0 < $6 >>> 0 ? 0 : 1; + $6 = $6 >> 31; + if (($3 | 0) > ($6 | 0) ? 1 : ($3 | 0) >= ($6 | 0) ? $7 : 0) { + break label$5 + } + HEAP32[$0 + 104 >> 2] = $4 + $1; + break label$4; + } + HEAP32[$0 + 104 >> 2] = $2; + } + label$6 : { + if (!$2) { + $2 = HEAP32[$0 + 4 >> 2]; + break label$6; + } + $3 = $0; + $1 = $2; + $2 = HEAP32[$0 + 4 >> 2]; + $1 = ($1 - $2 | 0) + 1 | 0; + $4 = $1 + HEAP32[$0 + 120 >> 2] | 0; + $0 = HEAP32[$0 + 124 >> 2] + ($1 >> 31) | 0; + HEAP32[$3 + 120 >> 2] = $4; + HEAP32[$3 + 124 >> 2] = $4 >>> 0 < $1 >>> 0 ? $0 + 1 | 0 : $0; + } + $0 = $2 + -1 | 0; + if (HEAPU8[$0 | 0] != ($5 | 0)) { + HEAP8[$0 | 0] = $5 + } + return $5; + } + + function __extendsftf2($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + $4 = global$0 - 16 | 0; + global$0 = $4; + $5 = (wasm2js_scratch_store_f32($1), wasm2js_scratch_load_i32(0)); + $2 = $5 & 2147483647; + label$1 : { + if ($2 + -8388608 >>> 0 <= 2130706431) { + $3 = $2; + $2 = $2 >>> 7 | 0; + $3 = $3 << 25; + $2 = $2 + 1065353216 | 0; + $6 = $3; + $2 = $3 >>> 0 < 0 ? $2 + 1 | 0 : $2; + break label$1; + } + if ($2 >>> 0 >= 2139095040) { + $2 = $5; + $3 = $2 >>> 7 | 0; + $6 = $2 << 25; + $2 = $3 | 2147418112; + break label$1; + } + if (!$2) { + $2 = 0; + break label$1; + } + $3 = $2; + $2 = Math_clz32($2); + __ashlti3($4, $3, 0, 0, 0, $2 + 81 | 0); + $7 = HEAP32[$4 >> 2]; + $8 = HEAP32[$4 + 4 >> 2]; + $6 = HEAP32[$4 + 8 >> 2]; + $2 = HEAP32[$4 + 12 >> 2] ^ 65536 | 16265 - $2 << 16; + } + HEAP32[$0 >> 2] = $7; + HEAP32[$0 + 4 >> 2] = $8; + HEAP32[$0 + 8 >> 2] = $6; + HEAP32[$0 + 12 >> 2] = $5 & -2147483648 | $2; + global$0 = $4 + 16 | 0; + } + + function __floatsitf($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $3 = global$0 - 16 | 0; + global$0 = $3; + $6 = $0; + $7 = $0; + label$1 : { + if (!$1) { + $1 = 0; + $5 = 0; + break label$1; + } + $2 = $1 >> 31; + $4 = $2 + $1 ^ $2; + $2 = Math_clz32($4); + __ashlti3($3, $4, 0, 0, 0, $2 + 81 | 0); + $2 = (HEAP32[$3 + 12 >> 2] ^ 65536) + (16414 - $2 << 16) | 0; + $4 = 0 + HEAP32[$3 + 8 >> 2] | 0; + if ($4 >>> 0 < $5 >>> 0) { + $2 = $2 + 1 | 0 + } + $1 = $1 & -2147483648 | $2; + $2 = HEAP32[$3 + 4 >> 2]; + $5 = HEAP32[$3 >> 2]; + } + HEAP32[$7 >> 2] = $5; + HEAP32[$6 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $4; + HEAP32[$0 + 12 >> 2] = $1; + global$0 = $3 + 16 | 0; + } + + function __multf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0, $46 = 0, $47 = 0; + $13 = global$0 - 96 | 0; + global$0 = $13; + $15 = $2; + $10 = $6; + $19 = ($10 & 131071) << 15 | $5 >>> 17; + $9 = $8 & 65535; + $21 = $9; + $17 = $7; + $10 = $7; + $24 = $10 << 15 | $6 >>> 17; + $14 = ($4 ^ $8) & -2147483648; + $10 = $4 & 65535; + $12 = $10; + $16 = $3; + $27 = $10; + $10 = $9; + $25 = ($10 & 131071) << 15 | $7 >>> 17; + $37 = $8 >>> 16 & 32767; + $38 = $4 >>> 16 & 32767; + label$1 : { + label$2 : { + if ($38 + -1 >>> 0 <= 32765) { + $20 = 0; + if ($37 + -1 >>> 0 < 32766) { + break label$2 + } + } + $11 = $4 & 2147483647; + $9 = $11; + $10 = $3; + if (!(!$3 & ($9 | 0) == 2147418112 ? !($1 | $2) : ($9 | 0) == 2147418112 & $3 >>> 0 < 0 | $9 >>> 0 < 2147418112)) { + $22 = $3; + $14 = $4 | 32768; + break label$1; + } + $11 = $8 & 2147483647; + $4 = $11; + $3 = $7; + if (!(!$3 & ($4 | 0) == 2147418112 ? !($5 | $6) : ($4 | 0) == 2147418112 & $3 >>> 0 < 0 | $4 >>> 0 < 2147418112)) { + $22 = $7; + $14 = $8 | 32768; + $1 = $5; + $2 = $6; + break label$1; + } + if (!($1 | $10 | ($9 ^ 2147418112 | $2))) { + if (!($3 | $5 | ($4 | $6))) { + $14 = 2147450880; + $1 = 0; + $2 = 0; + break label$1; + } + $14 = $14 | 2147418112; + $1 = 0; + $2 = 0; + break label$1; + } + if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { + $3 = $1 | $10; + $4 = $2 | $9; + $1 = 0; + $2 = 0; + if (!($3 | $4)) { + $14 = 2147450880; + break label$1; + } + $14 = $14 | 2147418112; + break label$1; + } + if (!($1 | $10 | ($2 | $9))) { + $1 = 0; + $2 = 0; + break label$1; + } + if (!($3 | $5 | ($4 | $6))) { + $1 = 0; + $2 = 0; + break label$1; + } + $3 = 0; + if (($9 | 0) == 65535 | $9 >>> 0 < 65535) { + $9 = $1; + $8 = $2; + $3 = !($12 | $16); + $7 = $3 << 6; + $10 = Math_clz32($3 ? $1 : $16) + 32 | 0; + $1 = Math_clz32($3 ? $2 : $12); + $1 = $7 + (($1 | 0) == 32 ? $10 : $1) | 0; + __ashlti3($13 + 80 | 0, $9, $8, $16, $12, $1 + -15 | 0); + $16 = HEAP32[$13 + 88 >> 2]; + $15 = HEAP32[$13 + 84 >> 2]; + $27 = HEAP32[$13 + 92 >> 2]; + $3 = 16 - $1 | 0; + $1 = HEAP32[$13 + 80 >> 2]; + } + $20 = $3; + if ($4 >>> 0 > 65535) { + break label$2 + } + $2 = !($17 | $21); + $4 = $2 << 6; + $7 = Math_clz32($2 ? $5 : $17) + 32 | 0; + $2 = Math_clz32($2 ? $6 : $21); + $2 = $4 + (($2 | 0) == 32 ? $7 : $2) | 0; + $8 = $2; + __ashlti3($13 - -64 | 0, $5, $6, $17, $21, $2 + -15 | 0); + $5 = HEAP32[$13 + 76 >> 2]; + $2 = $5; + $7 = HEAP32[$13 + 72 >> 2]; + $4 = $7; + $4 = $4 << 15; + $10 = HEAP32[$13 + 68 >> 2]; + $24 = $10 >>> 17 | $4; + $4 = $10; + $5 = HEAP32[$13 + 64 >> 2]; + $19 = ($4 & 131071) << 15 | $5 >>> 17; + $25 = ($2 & 131071) << 15 | $7 >>> 17; + $20 = ($3 - $8 | 0) + 16 | 0; + } + $3 = $19; + $17 = 0; + $8 = __wasm_i64_mul($3, 0, $1, $17); + $2 = i64toi32_i32$HIGH_BITS; + $26 = $2; + $23 = $5 << 15 & -32768; + $5 = __wasm_i64_mul($23, 0, $15, 0); + $4 = $5 + $8 | 0; + $11 = i64toi32_i32$HIGH_BITS + $2 | 0; + $11 = $4 >>> 0 < $5 >>> 0 ? $11 + 1 | 0 : $11; + $2 = $4; + $5 = 0; + $6 = __wasm_i64_mul($23, $28, $1, $17); + $4 = $5 + $6 | 0; + $9 = i64toi32_i32$HIGH_BITS + $2 | 0; + $9 = $4 >>> 0 < $6 >>> 0 ? $9 + 1 | 0 : $9; + $19 = $4; + $6 = $9; + $32 = ($2 | 0) == ($9 | 0) & $4 >>> 0 < $5 >>> 0 | $9 >>> 0 < $2 >>> 0; + $41 = __wasm_i64_mul($3, $39, $15, $40); + $33 = i64toi32_i32$HIGH_BITS; + $29 = $16; + $5 = __wasm_i64_mul($23, $28, $16, 0); + $4 = $5 + $41 | 0; + $12 = i64toi32_i32$HIGH_BITS + $33 | 0; + $12 = $4 >>> 0 < $5 >>> 0 ? $12 + 1 | 0 : $12; + $42 = $4; + $7 = __wasm_i64_mul($24, 0, $1, $17); + $4 = $4 + $7 | 0; + $5 = i64toi32_i32$HIGH_BITS + $12 | 0; + $34 = $4; + $5 = $4 >>> 0 < $7 >>> 0 ? $5 + 1 | 0 : $5; + $21 = $5; + $7 = $5; + $5 = ($11 | 0) == ($26 | 0) & $2 >>> 0 < $8 >>> 0 | $11 >>> 0 < $26 >>> 0; + $4 = $11; + $2 = $4 + $34 | 0; + $9 = $5 + $7 | 0; + $26 = $2; + $9 = $2 >>> 0 < $4 >>> 0 ? $9 + 1 | 0 : $9; + $4 = $9; + $7 = $2; + $44 = __wasm_i64_mul($3, $39, $16, $43); + $35 = i64toi32_i32$HIGH_BITS; + $2 = $23; + $30 = $27 | 65536; + $23 = $18; + $5 = __wasm_i64_mul($2, $28, $30, $18); + $2 = $5 + $44 | 0; + $9 = i64toi32_i32$HIGH_BITS + $35 | 0; + $9 = $2 >>> 0 < $5 >>> 0 ? $9 + 1 | 0 : $9; + $45 = $2; + $10 = __wasm_i64_mul($15, $40, $24, $46); + $2 = $2 + $10 | 0; + $18 = $9; + $5 = $9 + i64toi32_i32$HIGH_BITS | 0; + $5 = $2 >>> 0 < $10 >>> 0 ? $5 + 1 | 0 : $5; + $36 = $2; + $31 = $25 & 2147483647 | -2147483648; + $2 = __wasm_i64_mul($31, 0, $1, $17); + $1 = $36 + $2 | 0; + $17 = $5; + $10 = $5 + i64toi32_i32$HIGH_BITS | 0; + $28 = $1; + $2 = $1 >>> 0 < $2 >>> 0 ? $10 + 1 | 0 : $10; + $9 = $4 + $1 | 0; + $5 = 0; + $1 = $5 + $7 | 0; + if ($1 >>> 0 < $5 >>> 0) { + $9 = $9 + 1 | 0 + } + $27 = $1; + $25 = $9; + $5 = $9; + $7 = $1 + $32 | 0; + if ($7 >>> 0 < $1 >>> 0) { + $5 = $5 + 1 | 0 + } + $8 = $5; + $16 = ($20 + ($37 + $38 | 0) | 0) + -16383 | 0; + $5 = __wasm_i64_mul($29, $43, $24, $46); + $1 = i64toi32_i32$HIGH_BITS; + $11 = 0; + $10 = __wasm_i64_mul($3, $39, $30, $23); + $3 = $10 + $5 | 0; + $9 = i64toi32_i32$HIGH_BITS + $1 | 0; + $9 = $3 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; + $20 = $3; + $10 = $3; + $3 = $9; + $9 = ($1 | 0) == ($3 | 0) & $10 >>> 0 < $5 >>> 0 | $3 >>> 0 < $1 >>> 0; + $5 = __wasm_i64_mul($31, $47, $15, $40); + $1 = $5 + $10 | 0; + $10 = i64toi32_i32$HIGH_BITS + $3 | 0; + $10 = $1 >>> 0 < $5 >>> 0 ? $10 + 1 | 0 : $10; + $15 = $1; + $5 = $1; + $1 = $10; + $3 = ($3 | 0) == ($1 | 0) & $5 >>> 0 < $20 >>> 0 | $1 >>> 0 < $3 >>> 0; + $5 = $9 + $3 | 0; + if ($5 >>> 0 < $3 >>> 0) { + $11 = 1 + } + $10 = $5; + $3 = $1; + $5 = $11; + $32 = $10; + $9 = 0; + $10 = ($12 | 0) == ($21 | 0) & $34 >>> 0 < $42 >>> 0 | $21 >>> 0 < $12 >>> 0; + $12 = $10 + (($12 | 0) == ($33 | 0) & $42 >>> 0 < $41 >>> 0 | $12 >>> 0 < $33 >>> 0) | 0; + if ($12 >>> 0 < $10 >>> 0) { + $9 = 1 + } + $11 = $12; + $12 = $12 + $15 | 0; + $10 = $3 + $9 | 0; + $20 = $12; + $9 = $12; + $10 = $9 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; + $3 = $10; + $1 = ($1 | 0) == ($3 | 0) & $9 >>> 0 < $15 >>> 0 | $3 >>> 0 < $1 >>> 0; + $10 = $32 + $1 | 0; + if ($10 >>> 0 < $1 >>> 0) { + $5 = $5 + 1 | 0 + } + $1 = $10; + $10 = __wasm_i64_mul($31, $47, $30, $23); + $1 = $1 + $10 | 0; + $9 = i64toi32_i32$HIGH_BITS + $5 | 0; + $9 = $1 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; + $11 = $1; + $12 = __wasm_i64_mul($31, $47, $29, $43); + $5 = i64toi32_i32$HIGH_BITS; + $15 = __wasm_i64_mul($24, $46, $30, $23); + $1 = $15 + $12 | 0; + $10 = i64toi32_i32$HIGH_BITS + $5 | 0; + $10 = $1 >>> 0 < $15 >>> 0 ? $10 + 1 | 0 : $10; + $15 = $1; + $1 = $10; + $10 = ($5 | 0) == ($1 | 0) & $15 >>> 0 < $12 >>> 0 | $1 >>> 0 < $5 >>> 0; + $5 = $1 + $11 | 0; + $11 = $9 + $10 | 0; + $10 = $5 >>> 0 < $1 >>> 0 ? $11 + 1 | 0 : $11; + $29 = $5; + $9 = $3 + $15 | 0; + $11 = 0; + $1 = $11 + $20 | 0; + if ($1 >>> 0 < $11 >>> 0) { + $9 = $9 + 1 | 0 + } + $12 = $1; + $5 = $1; + $1 = $9; + $3 = ($3 | 0) == ($1 | 0) & $5 >>> 0 < $20 >>> 0 | $1 >>> 0 < $3 >>> 0; + $5 = $29 + $3 | 0; + if ($5 >>> 0 < $3 >>> 0) { + $10 = $10 + 1 | 0 + } + $15 = $5; + $11 = $1; + $9 = 0; + $5 = ($18 | 0) == ($17 | 0) & $36 >>> 0 < $45 >>> 0 | $17 >>> 0 < $18 >>> 0; + $18 = $5 + (($18 | 0) == ($35 | 0) & $45 >>> 0 < $44 >>> 0 | $18 >>> 0 < $35 >>> 0) | 0; + if ($18 >>> 0 < $5 >>> 0) { + $9 = 1 + } + $5 = $18 + (($2 | 0) == ($17 | 0) & $28 >>> 0 < $36 >>> 0 | $2 >>> 0 < $17 >>> 0) | 0; + $3 = $2; + $2 = $3 + $12 | 0; + $11 = $5 + $11 | 0; + $11 = $2 >>> 0 < $3 >>> 0 ? $11 + 1 | 0 : $11; + $18 = $2; + $3 = $2; + $2 = $11; + $1 = ($1 | 0) == ($2 | 0) & $3 >>> 0 < $12 >>> 0 | $2 >>> 0 < $1 >>> 0; + $3 = $1 + $15 | 0; + if ($3 >>> 0 < $1 >>> 0) { + $10 = $10 + 1 | 0 + } + $1 = $2; + $9 = $10; + $10 = $3; + $5 = 0; + $3 = ($4 | 0) == ($25 | 0) & $27 >>> 0 < $26 >>> 0 | $25 >>> 0 < $4 >>> 0; + $4 = $3 + (($4 | 0) == ($21 | 0) & $26 >>> 0 < $34 >>> 0 | $4 >>> 0 < $21 >>> 0) | 0; + if ($4 >>> 0 < $3 >>> 0) { + $5 = 1 + } + $3 = $4 + $18 | 0; + $11 = $1 + $5 | 0; + $11 = $3 >>> 0 < $4 >>> 0 ? $11 + 1 | 0 : $11; + $1 = $3; + $4 = $11; + $1 = ($2 | 0) == ($4 | 0) & $1 >>> 0 < $18 >>> 0 | $4 >>> 0 < $2 >>> 0; + $2 = $10 + $1 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $9 = $9 + 1 | 0 + } + $1 = $2; + $2 = $9; + label$13 : { + if ($2 & 65536) { + $16 = $16 + 1 | 0; + break label$13; + } + $12 = $6 >>> 31 | 0; + $9 = $2 << 1 | $1 >>> 31; + $1 = $1 << 1 | $4 >>> 31; + $2 = $9; + $9 = $4 << 1 | $3 >>> 31; + $3 = $3 << 1 | $8 >>> 31; + $4 = $9; + $10 = $19; + $9 = $6 << 1 | $10 >>> 31; + $19 = $10 << 1; + $6 = $9; + $10 = $8 << 1 | $7 >>> 31; + $7 = $7 << 1 | $12; + $8 = $10; + } + if (($16 | 0) >= 32767) { + $14 = $14 | 2147418112; + $1 = 0; + $2 = 0; + break label$1; + } + label$16 : { + if (($16 | 0) <= 0) { + $5 = 1 - $16 | 0; + if ($5 >>> 0 <= 127) { + $10 = $16 + 127 | 0; + __ashlti3($13 + 48 | 0, $19, $6, $7, $8, $10); + __ashlti3($13 + 32 | 0, $3, $4, $1, $2, $10); + __lshrti3($13 + 16 | 0, $19, $6, $7, $8, $5); + __lshrti3($13, $3, $4, $1, $2, $5); + $19 = (HEAP32[$13 + 48 >> 2] | HEAP32[$13 + 56 >> 2]) != 0 | (HEAP32[$13 + 52 >> 2] | HEAP32[$13 + 60 >> 2]) != 0 | (HEAP32[$13 + 32 >> 2] | HEAP32[$13 + 16 >> 2]); + $6 = HEAP32[$13 + 36 >> 2] | HEAP32[$13 + 20 >> 2]; + $7 = HEAP32[$13 + 40 >> 2] | HEAP32[$13 + 24 >> 2]; + $8 = HEAP32[$13 + 44 >> 2] | HEAP32[$13 + 28 >> 2]; + $3 = HEAP32[$13 >> 2]; + $4 = HEAP32[$13 + 4 >> 2]; + $2 = HEAP32[$13 + 12 >> 2]; + $1 = HEAP32[$13 + 8 >> 2]; + break label$16; + } + $1 = 0; + $2 = 0; + break label$1; + } + $2 = $2 & 65535 | $16 << 16; + } + $22 = $1 | $22; + $14 = $2 | $14; + if (!(!$7 & ($8 | 0) == -2147483648 ? !($6 | $19) : ($8 | 0) > -1 ? 1 : 0)) { + $11 = $14; + $12 = $4; + $1 = $3 + 1 | 0; + if ($1 >>> 0 < 1) { + $12 = $12 + 1 | 0 + } + $2 = $12; + $3 = ($4 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $4 >>> 0; + $4 = $3 + $22 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $11 = $11 + 1 | 0 + } + $22 = $4; + $14 = $11; + break label$1; + } + if ($7 | $19 | ($8 ^ -2147483648 | $6)) { + $1 = $3; + $2 = $4; + break label$1; + } + $12 = $14; + $9 = $4; + $1 = $3 & 1; + $2 = $1 + $3 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $9 = $9 + 1 | 0 + } + $1 = $2; + $2 = $9; + $3 = ($4 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $4 >>> 0; + $4 = $3 + $22 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $12 = $12 + 1 | 0 + } + $22 = $4; + $14 = $12; + } + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $22; + HEAP32[$0 + 12 >> 2] = $14; + global$0 = $13 + 96 | 0; + } + + function __addtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0; + $11 = global$0 - 112 | 0; + global$0 = $11; + $12 = $7; + $14 = $8 & 2147483647; + $10 = $2 + -1 | 0; + $9 = $1 + -1 | 0; + if (($9 | 0) != -1) { + $10 = $10 + 1 | 0 + } + $13 = $9; + $17 = ($9 | 0) == -1 & ($10 | 0) == -1; + $15 = $4 & 2147483647; + $9 = $15; + $16 = $3; + $10 = ($2 | 0) == ($10 | 0) & $13 >>> 0 < $1 >>> 0 | $10 >>> 0 < $2 >>> 0; + $13 = $3 + $10 | 0; + if ($13 >>> 0 < $10 >>> 0) { + $9 = $9 + 1 | 0 + } + $13 = $13 + -1 | 0; + $10 = $9 + -1 | 0; + $9 = $13; + label$1 : { + label$2 : { + $10 = ($9 | 0) != -1 ? $10 + 1 | 0 : $10; + if (!(($9 | 0) == -1 & ($10 | 0) == 2147418111 ? $17 : $10 >>> 0 > 2147418111)) { + $10 = $6 + -1 | 0; + $9 = $5 + -1 | 0; + if (($9 | 0) != -1) { + $10 = $10 + 1 | 0 + } + $13 = $9; + $17 = ($9 | 0) != -1 | ($10 | 0) != -1; + $9 = $14; + $10 = ($6 | 0) == ($10 | 0) & $13 >>> 0 < $5 >>> 0 | $10 >>> 0 < $6 >>> 0; + $13 = $10 + $12 | 0; + if ($13 >>> 0 < $10 >>> 0) { + $9 = $9 + 1 | 0 + } + $10 = $13 + -1 | 0; + $9 = $9 + -1 | 0; + $9 = ($10 | 0) != -1 ? $9 + 1 | 0 : $9; + if (($10 | 0) == -1 & ($9 | 0) == 2147418111 ? $17 : ($9 | 0) == 2147418111 & ($10 | 0) != -1 | $9 >>> 0 < 2147418111) { + break label$2 + } + } + if (!(!$16 & ($15 | 0) == 2147418112 ? !($1 | $2) : ($15 | 0) == 2147418112 & $16 >>> 0 < 0 | $15 >>> 0 < 2147418112)) { + $7 = $3; + $8 = $4 | 32768; + $5 = $1; + $6 = $2; + break label$1; + } + if (!(!$12 & ($14 | 0) == 2147418112 ? !($5 | $6) : ($14 | 0) == 2147418112 & $12 >>> 0 < 0 | $14 >>> 0 < 2147418112)) { + $8 = $8 | 32768; + break label$1; + } + if (!($1 | $16 | ($15 ^ 2147418112 | $2))) { + $9 = $3; + $3 = !($1 ^ $5 | $3 ^ $7 | ($2 ^ $6 | $4 ^ $8 ^ -2147483648)); + $7 = $3 ? 0 : $9; + $8 = $3 ? 2147450880 : $4; + $5 = $3 ? 0 : $1; + $6 = $3 ? 0 : $2; + break label$1; + } + if (!($5 | $12 | ($14 ^ 2147418112 | $6))) { + break label$1 + } + if (!($1 | $16 | ($2 | $15))) { + if ($5 | $12 | ($6 | $14)) { + break label$1 + } + $5 = $1 & $5; + $6 = $2 & $6; + $7 = $3 & $7; + $8 = $4 & $8; + break label$1; + } + if ($5 | $12 | ($6 | $14)) { + break label$2 + } + $5 = $1; + $6 = $2; + $7 = $3; + $8 = $4; + break label$1; + } + $10 = ($12 | 0) == ($16 | 0) & ($14 | 0) == ($15 | 0) ? ($2 | 0) == ($6 | 0) & $5 >>> 0 > $1 >>> 0 | $6 >>> 0 > $2 >>> 0 : ($14 | 0) == ($15 | 0) & $12 >>> 0 > $16 >>> 0 | $14 >>> 0 > $15 >>> 0; + $9 = $10; + $15 = $9 ? $5 : $1; + $14 = $9 ? $6 : $2; + $12 = $9 ? $8 : $4; + $16 = $12; + $13 = $9 ? $7 : $3; + $9 = $12 & 65535; + $4 = $10 ? $4 : $8; + $18 = $4; + $3 = $10 ? $3 : $7; + $17 = $4 >>> 16 & 32767; + $12 = $12 >>> 16 & 32767; + if (!$12) { + $4 = !($9 | $13); + $7 = $4 << 6; + $8 = Math_clz32($4 ? $15 : $13) + 32 | 0; + $4 = Math_clz32($4 ? $14 : $9); + $4 = $7 + (($4 | 0) == 32 ? $8 : $4) | 0; + __ashlti3($11 + 96 | 0, $15, $14, $13, $9, $4 + -15 | 0); + $13 = HEAP32[$11 + 104 >> 2]; + $15 = HEAP32[$11 + 96 >> 2]; + $14 = HEAP32[$11 + 100 >> 2]; + $12 = 16 - $4 | 0; + $9 = HEAP32[$11 + 108 >> 2]; + } + $5 = $10 ? $1 : $5; + $6 = $10 ? $2 : $6; + $1 = $3; + $2 = $18 & 65535; + if ($17) { + $1 = $2 + } else { + $7 = $1; + $3 = !($1 | $2); + $4 = $3 << 6; + $8 = Math_clz32($3 ? $5 : $1) + 32 | 0; + $1 = Math_clz32($3 ? $6 : $2); + $1 = $4 + (($1 | 0) == 32 ? $8 : $1) | 0; + __ashlti3($11 + 80 | 0, $5, $6, $7, $2, $1 + -15 | 0); + $17 = 16 - $1 | 0; + $5 = HEAP32[$11 + 80 >> 2]; + $6 = HEAP32[$11 + 84 >> 2]; + $3 = HEAP32[$11 + 88 >> 2]; + $1 = HEAP32[$11 + 92 >> 2]; + } + $2 = $3; + $10 = $1 << 3 | $2 >>> 29; + $7 = $2 << 3 | $6 >>> 29; + $8 = $10 | 524288; + $1 = $13; + $3 = $9 << 3 | $1 >>> 29; + $4 = $1 << 3 | $14 >>> 29; + $13 = $3; + $10 = $16 ^ $18; + $1 = $5; + $9 = $6 << 3 | $1 >>> 29; + $1 = $1 << 3; + $2 = $9; + $5 = $12 - $17 | 0; + $3 = $1; + label$11 : { + if (!$5) { + break label$11 + } + if ($5 >>> 0 > 127) { + $7 = 0; + $8 = 0; + $9 = 0; + $3 = 1; + break label$11; + } + __ashlti3($11 - -64 | 0, $1, $2, $7, $8, 128 - $5 | 0); + __lshrti3($11 + 48 | 0, $1, $2, $7, $8, $5); + $7 = HEAP32[$11 + 56 >> 2]; + $8 = HEAP32[$11 + 60 >> 2]; + $9 = HEAP32[$11 + 52 >> 2]; + $3 = HEAP32[$11 + 48 >> 2] | ((HEAP32[$11 + 64 >> 2] | HEAP32[$11 + 72 >> 2]) != 0 | (HEAP32[$11 + 68 >> 2] | HEAP32[$11 + 76 >> 2]) != 0); + } + $6 = $9; + $13 = $13 | 524288; + $1 = $15; + $9 = $14 << 3 | $1 >>> 29; + $2 = $1 << 3; + label$13 : { + if (($10 | 0) < -1 ? 1 : ($10 | 0) <= -1 ? 1 : 0) { + $14 = $3; + $1 = $2 - $3 | 0; + $15 = $4 - $7 | 0; + $3 = ($6 | 0) == ($9 | 0) & $2 >>> 0 < $3 >>> 0 | $9 >>> 0 < $6 >>> 0; + $5 = $15 - $3 | 0; + $2 = $9 - (($2 >>> 0 < $14 >>> 0) + $6 | 0) | 0; + $6 = ($13 - (($4 >>> 0 < $7 >>> 0) + $8 | 0) | 0) - ($15 >>> 0 < $3 >>> 0) | 0; + if (!($1 | $5 | ($2 | $6))) { + $5 = 0; + $6 = 0; + $7 = 0; + $8 = 0; + break label$1; + } + if ($6 >>> 0 > 524287) { + break label$13 + } + $7 = $1; + $3 = !($5 | $6); + $4 = $3 << 6; + $8 = Math_clz32($3 ? $1 : $5) + 32 | 0; + $1 = Math_clz32($3 ? $2 : $6); + $1 = $4 + (($1 | 0) == 32 ? $8 : $1) | 0; + $1 = $1 + -12 | 0; + __ashlti3($11 + 32 | 0, $7, $2, $5, $6, $1); + $12 = $12 - $1 | 0; + $5 = HEAP32[$11 + 40 >> 2]; + $6 = HEAP32[$11 + 44 >> 2]; + $1 = HEAP32[$11 + 32 >> 2]; + $2 = HEAP32[$11 + 36 >> 2]; + break label$13; + } + $10 = $6 + $9 | 0; + $1 = $3; + $2 = $1 + $2 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $10 = $10 + 1 | 0 + } + $1 = $2; + $2 = $10; + $6 = ($6 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $6 >>> 0; + $10 = $8 + $13 | 0; + $3 = $4 + $7 | 0; + if ($3 >>> 0 < $4 >>> 0) { + $10 = $10 + 1 | 0 + } + $5 = $3; + $4 = $6 + $3 | 0; + $3 = $10; + $3 = $4 >>> 0 < $5 >>> 0 ? $3 + 1 | 0 : $3; + $5 = $4; + $6 = $3; + if (!($3 & 1048576)) { + break label$13 + } + $1 = $1 & 1 | (($2 & 1) << 31 | $1 >>> 1); + $2 = $5 << 31 | $2 >>> 1; + $12 = $12 + 1 | 0; + $5 = ($6 & 1) << 31 | $5 >>> 1; + $6 = $6 >>> 1 | 0; + } + $7 = 0; + $9 = $16 & -2147483648; + if (($12 | 0) >= 32767) { + $8 = $9 | 2147418112; + $5 = 0; + $6 = 0; + break label$1; + } + $4 = 0; + label$17 : { + if (($12 | 0) > 0) { + $4 = $12; + break label$17; + } + __ashlti3($11 + 16 | 0, $1, $2, $5, $6, $12 + 127 | 0); + __lshrti3($11, $1, $2, $5, $6, 1 - $12 | 0); + $1 = HEAP32[$11 >> 2] | ((HEAP32[$11 + 16 >> 2] | HEAP32[$11 + 24 >> 2]) != 0 | (HEAP32[$11 + 20 >> 2] | HEAP32[$11 + 28 >> 2]) != 0); + $2 = HEAP32[$11 + 4 >> 2]; + $5 = HEAP32[$11 + 8 >> 2]; + $6 = HEAP32[$11 + 12 >> 2]; + } + $7 = $7 | (($6 & 7) << 29 | $5 >>> 3); + $4 = $9 | $6 >>> 3 & 65535 | $4 << 16; + $9 = $5 << 29; + $3 = 0; + $5 = $9; + $6 = ($2 & 7) << 29 | $1 >>> 3 | $3; + $9 = $4; + $3 = $2 >>> 3 | $5; + $10 = $3; + $4 = $1 & 7; + $1 = $4 >>> 0 > 4; + $2 = $1 + $6 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $10 = $10 + 1 | 0 + } + $1 = $2; + $2 = $10; + $3 = ($3 | 0) == ($2 | 0) & $1 >>> 0 < $6 >>> 0 | $2 >>> 0 < $3 >>> 0; + $5 = $3 + $7 | 0; + if ($5 >>> 0 < $3 >>> 0) { + $9 = $9 + 1 | 0 + } + $4 = ($4 | 0) == 4; + $3 = $4 ? $1 & 1 : 0; + $8 = $9; + $7 = $5; + $4 = 0; + $9 = $2 + $4 | 0; + $2 = $1 + $3 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $9 = $9 + 1 | 0 + } + $5 = $2; + $1 = $2; + $6 = $9; + $1 = ($4 | 0) == ($9 | 0) & $1 >>> 0 < $3 >>> 0 | $9 >>> 0 < $4 >>> 0; + $2 = $7 + $1 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $8 = $8 + 1 | 0 + } + $7 = $2; + } + HEAP32[$0 >> 2] = $5; + HEAP32[$0 + 4 >> 2] = $6; + HEAP32[$0 + 8 >> 2] = $7; + HEAP32[$0 + 12 >> 2] = $8; + global$0 = $11 + 112 | 0; + } + + function __extenddftf2($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $5 = global$0 - 16 | 0; + global$0 = $5; + wasm2js_scratch_store_f64(+$1); + $8 = wasm2js_scratch_load_i32(1) | 0; + $6 = wasm2js_scratch_load_i32(0) | 0; + $7 = $8 & 2147483647; + $2 = $7; + $4 = $2 + -1048576 | 0; + $3 = $6; + if ($3 >>> 0 < 0) { + $4 = $4 + 1 | 0 + } + label$1 : { + if (($4 | 0) == 2145386495 | $4 >>> 0 < 2145386495) { + $7 = $3 << 28; + $4 = ($2 & 15) << 28 | $3 >>> 4; + $2 = ($2 >>> 4 | 0) + 1006632960 | 0; + $3 = $4; + $2 = $3 >>> 0 < 0 ? $2 + 1 | 0 : $2; + break label$1; + } + if (($2 | 0) == 2146435072 & $3 >>> 0 >= 0 | $2 >>> 0 > 2146435072) { + $7 = $6 << 28; + $4 = $6; + $2 = $8; + $6 = $2 >>> 4 | 0; + $3 = ($2 & 15) << 28 | $4 >>> 4; + $2 = $6 | 2147418112; + break label$1; + } + if (!($2 | $3)) { + $7 = 0; + $3 = 0; + $2 = 0; + break label$1; + } + $4 = $2; + $2 = ($2 | 0) == 1 & $3 >>> 0 < 0 | $2 >>> 0 < 1 ? Math_clz32($6) + 32 | 0 : Math_clz32($2); + __ashlti3($5, $3, $4, 0, 0, $2 + 49 | 0); + $9 = HEAP32[$5 >> 2]; + $7 = HEAP32[$5 + 4 >> 2]; + $3 = HEAP32[$5 + 8 >> 2]; + $2 = HEAP32[$5 + 12 >> 2] ^ 65536 | 15372 - $2 << 16; + } + HEAP32[$0 >> 2] = $9; + HEAP32[$0 + 4 >> 2] = $7; + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $8 & -2147483648 | $2; + global$0 = $5 + 16 | 0; + } + + function __letf2($0, $1, $2, $3, $4, $5, $6, $7) { + var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; + $9 = 1; + $8 = $3 & 2147483647; + $12 = $8; + $10 = $2; + label$1 : { + if (!$2 & ($8 | 0) == 2147418112 ? $0 | $1 : ($8 | 0) == 2147418112 & $2 >>> 0 > 0 | $8 >>> 0 > 2147418112) { + break label$1 + } + $11 = $7 & 2147483647; + $13 = $11; + $8 = $6; + if (!$6 & ($11 | 0) == 2147418112 ? $4 | $5 : ($11 | 0) == 2147418112 & $6 >>> 0 > 0 | $11 >>> 0 > 2147418112) { + break label$1 + } + if (!($0 | $4 | ($8 | $10) | ($1 | $5 | ($12 | $13)))) { + return 0 + } + $10 = $3 & $7; + if (($10 | 0) > 0 ? 1 : ($10 | 0) >= 0 ? (($2 & $6) >>> 0 < 0 ? 0 : 1) : 0) { + $9 = -1; + if (($2 | 0) == ($6 | 0) & ($3 | 0) == ($7 | 0) ? ($1 | 0) == ($5 | 0) & $0 >>> 0 < $4 >>> 0 | $1 >>> 0 < $5 >>> 0 : ($3 | 0) < ($7 | 0) ? 1 : ($3 | 0) <= ($7 | 0) ? ($2 >>> 0 >= $6 >>> 0 ? 0 : 1) : 0) { + break label$1 + } + return ($0 ^ $4 | $2 ^ $6) != 0 | ($1 ^ $5 | $3 ^ $7) != 0; + } + $9 = -1; + if (($2 | 0) == ($6 | 0) & ($3 | 0) == ($7 | 0) ? ($1 | 0) == ($5 | 0) & $0 >>> 0 > $4 >>> 0 | $1 >>> 0 > $5 >>> 0 : ($3 | 0) > ($7 | 0) ? 1 : ($3 | 0) >= ($7 | 0) ? ($2 >>> 0 <= $6 >>> 0 ? 0 : 1) : 0) { + break label$1 + } + $9 = ($0 ^ $4 | $2 ^ $6) != 0 | ($1 ^ $5 | $3 ^ $7) != 0; + } + return $9; + } + + function __getf2($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $7 = -1; + $5 = $3 & 2147483647; + $8 = $5; + $6 = $2; + label$1 : { + if (!$2 & ($5 | 0) == 2147418112 ? $0 | $1 : ($5 | 0) == 2147418112 & $2 >>> 0 > 0 | $5 >>> 0 > 2147418112) { + break label$1 + } + $5 = $4 & 2147483647; + $9 = $5; + if (($5 | 0) == 2147418112 ? 0 : $5 >>> 0 > 2147418112) { + break label$1 + } + if (!($0 | $6 | ($1 | ($8 | $9)))) { + return 0 + } + $6 = $3 & $4; + if (($6 | 0) > 0 ? 1 : ($6 | 0) >= 0 ? 1 : 0) { + if (!$2 & ($3 | 0) == ($4 | 0) ? !$1 & $0 >>> 0 < 0 | $1 >>> 0 < 0 : ($3 | 0) < ($4 | 0) ? 1 : ($3 | 0) <= ($4 | 0) ? ($2 >>> 0 >= 0 ? 0 : 1) : 0) { + break label$1 + } + return ($0 | $2) != 0 | ($1 | $3 ^ $4) != 0; + } + if (!$2 & ($3 | 0) == ($4 | 0) ? !$1 & $0 >>> 0 > 0 | $1 >>> 0 > 0 : ($3 | 0) > ($4 | 0) ? 1 : ($3 | 0) >= ($4 | 0) ? ($2 >>> 0 <= 0 ? 0 : 1) : 0) { + break label$1 + } + $7 = ($0 | $2) != 0 | ($1 | $3 ^ $4) != 0; + } + return $7; + } + + function copysignl($0, $1, $2, $3, $4, $5, $6, $7, $8) { + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $4 & 65535 | ($8 >>> 16 & 32768 | $4 >>> 16 & 32767) << 16; + } + + function __floatunsitf($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; + $2 = global$0 - 16 | 0; + global$0 = $2; + $6 = $0; + $7 = $0; + label$1 : { + if (!$1) { + $1 = 0; + $3 = 0; + break label$1; + } + $3 = $1; + $1 = Math_clz32($1) ^ 31; + __ashlti3($2, $3, 0, 0, 0, 112 - $1 | 0); + $1 = (HEAP32[$2 + 12 >> 2] ^ 65536) + ($1 + 16383 << 16) | 0; + $4 = 0 + HEAP32[$2 + 8 >> 2] | 0; + if ($4 >>> 0 < $5 >>> 0) { + $1 = $1 + 1 | 0 + } + $5 = HEAP32[$2 + 4 >> 2]; + $3 = HEAP32[$2 >> 2]; + } + HEAP32[$7 >> 2] = $3; + HEAP32[$6 + 4 >> 2] = $5; + HEAP32[$0 + 8 >> 2] = $4; + HEAP32[$0 + 12 >> 2] = $1; + global$0 = $2 + 16 | 0; + } + + function __subtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0; + $9 = global$0 - 16 | 0; + global$0 = $9; + __addtf3($9, $1, $2, $3, $4, $5, $6, $7, $8 ^ -2147483648); + $1 = HEAP32[$9 + 4 >> 2]; + HEAP32[$0 >> 2] = HEAP32[$9 >> 2]; + HEAP32[$0 + 4 >> 2] = $1; + $1 = HEAP32[$9 + 12 >> 2]; + HEAP32[$0 + 8 >> 2] = HEAP32[$9 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $1; + global$0 = $9 + 16 | 0; + } + + function scalbnl($0, $1, $2, $3, $4, $5) { + var $6 = 0; + $6 = global$0 - 80 | 0; + global$0 = $6; + label$1 : { + if (($5 | 0) >= 16384) { + __multf3($6 + 32 | 0, $1, $2, $3, $4, 0, 0, 0, 2147352576); + $3 = HEAP32[$6 + 40 >> 2]; + $4 = HEAP32[$6 + 44 >> 2]; + $1 = HEAP32[$6 + 32 >> 2]; + $2 = HEAP32[$6 + 36 >> 2]; + if (($5 | 0) < 32767) { + $5 = $5 + -16383 | 0; + break label$1; + } + __multf3($6 + 16 | 0, $1, $2, $3, $4, 0, 0, 0, 2147352576); + $5 = (($5 | 0) < 49149 ? $5 : 49149) + -32766 | 0; + $3 = HEAP32[$6 + 24 >> 2]; + $4 = HEAP32[$6 + 28 >> 2]; + $1 = HEAP32[$6 + 16 >> 2]; + $2 = HEAP32[$6 + 20 >> 2]; + break label$1; + } + if (($5 | 0) > -16383) { + break label$1 + } + __multf3($6 - -64 | 0, $1, $2, $3, $4, 0, 0, 0, 65536); + $3 = HEAP32[$6 + 72 >> 2]; + $4 = HEAP32[$6 + 76 >> 2]; + $1 = HEAP32[$6 + 64 >> 2]; + $2 = HEAP32[$6 + 68 >> 2]; + if (($5 | 0) > -32765) { + $5 = $5 + 16382 | 0; + break label$1; + } + __multf3($6 + 48 | 0, $1, $2, $3, $4, 0, 0, 0, 65536); + $5 = (($5 | 0) > -49146 ? $5 : -49146) + 32764 | 0; + $3 = HEAP32[$6 + 56 >> 2]; + $4 = HEAP32[$6 + 60 >> 2]; + $1 = HEAP32[$6 + 48 >> 2]; + $2 = HEAP32[$6 + 52 >> 2]; + } + __multf3($6, $1, $2, $3, $4, 0, 0, 0, $5 + 16383 << 16); + $1 = HEAP32[$6 + 12 >> 2]; + HEAP32[$0 + 8 >> 2] = HEAP32[$6 + 8 >> 2]; + HEAP32[$0 + 12 >> 2] = $1; + $1 = HEAP32[$6 + 4 >> 2]; + HEAP32[$0 >> 2] = HEAP32[$6 >> 2]; + HEAP32[$0 + 4 >> 2] = $1; + global$0 = $6 + 80 | 0; + } + + function __multi3($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; + $5 = __wasm_i64_mul($1, $2, 0, 0); + $6 = i64toi32_i32$HIGH_BITS; + $7 = __wasm_i64_mul(0, 0, $3, $4); + $5 = $5 + $7 | 0; + $6 = i64toi32_i32$HIGH_BITS + $6 | 0; + $9 = __wasm_i64_mul($4, 0, $2, 0); + $8 = $5 + $9 | 0; + $5 = i64toi32_i32$HIGH_BITS + ($5 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6) | 0; + $6 = __wasm_i64_mul($3, 0, $1, 0); + $10 = i64toi32_i32$HIGH_BITS; + $7 = __wasm_i64_mul($2, 0, $3, 0); + $3 = $10 + $7 | 0; + $2 = $8 >>> 0 < $9 >>> 0 ? $5 + 1 | 0 : $5; + $5 = i64toi32_i32$HIGH_BITS; + $5 = $3 >>> 0 < $7 >>> 0 ? $5 + 1 | 0 : $5; + $8 = $5 + $8 | 0; + if ($8 >>> 0 < $5 >>> 0) { + $2 = $2 + 1 | 0 + } + $1 = __wasm_i64_mul($1, 0, $4, 0) + $3 | 0; + $4 = i64toi32_i32$HIGH_BITS; + $3 = $1 >>> 0 < $3 >>> 0 ? $4 + 1 | 0 : $4; + $4 = $8 + $3 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $2 = $2 + 1 | 0 + } + HEAP32[$0 + 8 >> 2] = $4; + HEAP32[$0 + 12 >> 2] = $2; + HEAP32[$0 >> 2] = $6; + HEAP32[$0 + 4 >> 2] = $1; + } + + function __divtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0; + $13 = global$0 - 192 | 0; + global$0 = $13; + $29 = $7; + $32 = $8 & 65535; + $16 = $3; + $14 = $4 & 65535; + $28 = ($4 ^ $8) & -2147483648; + $17 = $8 >>> 16 & 32767; + label$1 : { + $19 = $4 >>> 16 & 32767; + label$2 : { + label$3 : { + if ($19 + -1 >>> 0 <= 32765) { + if ($17 + -1 >>> 0 < 32766) { + break label$3 + } + } + $10 = $4 & 2147483647; + $11 = $10; + $9 = $3; + if (!(!$9 & ($10 | 0) == 2147418112 ? !($1 | $2) : ($10 | 0) == 2147418112 & $9 >>> 0 < 0 | $10 >>> 0 < 2147418112)) { + $33 = $3; + $28 = $4 | 32768; + break label$2; + } + $10 = $8 & 2147483647; + $4 = $10; + $3 = $7; + if (!(!$3 & ($10 | 0) == 2147418112 ? !($5 | $6) : ($10 | 0) == 2147418112 & $3 >>> 0 < 0 | $10 >>> 0 < 2147418112)) { + $33 = $7; + $28 = $8 | 32768; + $1 = $5; + $2 = $6; + break label$2; + } + if (!($1 | $9 | ($11 ^ 2147418112 | $2))) { + if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { + $1 = 0; + $2 = 0; + $28 = 2147450880; + break label$2; + } + $28 = $28 | 2147418112; + $1 = 0; + $2 = 0; + break label$2; + } + if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { + $1 = 0; + $2 = 0; + break label$2; + } + if (!($1 | $9 | ($2 | $11))) { + break label$1 + } + if (!($3 | $5 | ($4 | $6))) { + $28 = $28 | 2147418112; + $1 = 0; + $2 = 0; + break label$2; + } + $10 = 0; + if (($11 | 0) == 65535 | $11 >>> 0 < 65535) { + $8 = $1; + $3 = !($14 | $16); + $7 = $3 << 6; + $9 = Math_clz32($3 ? $1 : $16) + 32 | 0; + $1 = Math_clz32($3 ? $2 : $14); + $1 = $7 + (($1 | 0) == 32 ? $9 : $1) | 0; + __ashlti3($13 + 176 | 0, $8, $2, $16, $14, $1 + -15 | 0); + $10 = 16 - $1 | 0; + $16 = HEAP32[$13 + 184 >> 2]; + $14 = HEAP32[$13 + 188 >> 2]; + $2 = HEAP32[$13 + 180 >> 2]; + $1 = HEAP32[$13 + 176 >> 2]; + } + if ($4 >>> 0 > 65535) { + break label$3 + } + $3 = !($29 | $32); + $4 = $3 << 6; + $7 = Math_clz32($3 ? $5 : $29) + 32 | 0; + $3 = Math_clz32($3 ? $6 : $32); + $3 = $4 + (($3 | 0) == 32 ? $7 : $3) | 0; + __ashlti3($13 + 160 | 0, $5, $6, $29, $32, $3 + -15 | 0); + $10 = ($3 + $10 | 0) + -16 | 0; + $29 = HEAP32[$13 + 168 >> 2]; + $32 = HEAP32[$13 + 172 >> 2]; + $5 = HEAP32[$13 + 160 >> 2]; + $6 = HEAP32[$13 + 164 >> 2]; + } + $4 = $32 | 65536; + $31 = $4; + $38 = $29; + $3 = $29; + $12 = $4 << 15 | $3 >>> 17; + $3 = $3 << 15 | $6 >>> 17; + $7 = -102865788 - $3 | 0; + $4 = $12; + $9 = $4; + $8 = 1963258675 - ($9 + (4192101508 < $3 >>> 0) | 0) | 0; + __multi3($13 + 144 | 0, $3, $9, $7, $8); + $9 = HEAP32[$13 + 152 >> 2]; + __multi3($13 + 128 | 0, 0 - $9 | 0, 0 - (HEAP32[$13 + 156 >> 2] + (0 < $9 >>> 0) | 0) | 0, $7, $8); + $7 = HEAP32[$13 + 136 >> 2]; + $8 = $7 << 1 | HEAP32[$13 + 132 >> 2] >>> 31; + $7 = HEAP32[$13 + 140 >> 2] << 1 | $7 >>> 31; + __multi3($13 + 112 | 0, $8, $7, $3, $4); + $9 = $7; + $7 = HEAP32[$13 + 120 >> 2]; + __multi3($13 + 96 | 0, $8, $9, 0 - $7 | 0, 0 - (HEAP32[$13 + 124 >> 2] + (0 < $7 >>> 0) | 0) | 0); + $7 = HEAP32[$13 + 104 >> 2]; + $11 = HEAP32[$13 + 108 >> 2] << 1 | $7 >>> 31; + $8 = $7 << 1 | HEAP32[$13 + 100 >> 2] >>> 31; + __multi3($13 + 80 | 0, $8, $11, $3, $4); + $7 = HEAP32[$13 + 88 >> 2]; + __multi3($13 - -64 | 0, $8, $11, 0 - $7 | 0, 0 - (HEAP32[$13 + 92 >> 2] + (0 < $7 >>> 0) | 0) | 0); + $7 = HEAP32[$13 + 72 >> 2]; + $8 = $7 << 1 | HEAP32[$13 + 68 >> 2] >>> 31; + $7 = HEAP32[$13 + 76 >> 2] << 1 | $7 >>> 31; + __multi3($13 + 48 | 0, $8, $7, $3, $4); + $9 = $7; + $7 = HEAP32[$13 + 56 >> 2]; + __multi3($13 + 32 | 0, $8, $9, 0 - $7 | 0, 0 - (HEAP32[$13 + 60 >> 2] + (0 < $7 >>> 0) | 0) | 0); + $7 = HEAP32[$13 + 40 >> 2]; + $11 = HEAP32[$13 + 44 >> 2] << 1 | $7 >>> 31; + $8 = $7 << 1 | HEAP32[$13 + 36 >> 2] >>> 31; + __multi3($13 + 16 | 0, $8, $11, $3, $4); + $7 = HEAP32[$13 + 24 >> 2]; + __multi3($13, $8, $11, 0 - $7 | 0, 0 - (HEAP32[$13 + 28 >> 2] + (0 < $7 >>> 0) | 0) | 0); + $34 = ($19 - $17 | 0) + $10 | 0; + $7 = HEAP32[$13 + 8 >> 2]; + $9 = HEAP32[$13 + 12 >> 2] << 1 | $7 >>> 31; + $8 = $7 << 1; + $10 = $9 + -1 | 0; + $8 = (HEAP32[$13 + 4 >> 2] >>> 31 | $8) + -1 | 0; + if (($8 | 0) != -1) { + $10 = $10 + 1 | 0 + } + $7 = $8; + $9 = 0; + $21 = $9; + $20 = $4; + $11 = 0; + $12 = __wasm_i64_mul($7, $9, $4, $11); + $4 = i64toi32_i32$HIGH_BITS; + $19 = $4; + $22 = $10; + $17 = 0; + $9 = $3; + $7 = __wasm_i64_mul($10, $17, $9, 0); + $3 = $7 + $12 | 0; + $10 = i64toi32_i32$HIGH_BITS + $4 | 0; + $10 = $3 >>> 0 < $7 >>> 0 ? $10 + 1 | 0 : $10; + $7 = $3; + $3 = $10; + $15 = __wasm_i64_mul($8, $21, $9, $15); + $4 = 0 + $15 | 0; + $10 = $7; + $9 = $10 + i64toi32_i32$HIGH_BITS | 0; + $9 = $4 >>> 0 < $15 >>> 0 ? $9 + 1 | 0 : $9; + $15 = $4; + $4 = $9; + $9 = ($10 | 0) == ($9 | 0) & $15 >>> 0 < $23 >>> 0 | $9 >>> 0 < $10 >>> 0; + $10 = ($3 | 0) == ($19 | 0) & $10 >>> 0 < $12 >>> 0 | $3 >>> 0 < $19 >>> 0; + $7 = $3; + $3 = __wasm_i64_mul($22, $17, $20, $11) + $3 | 0; + $11 = $10 + i64toi32_i32$HIGH_BITS | 0; + $11 = $3 >>> 0 < $7 >>> 0 ? $11 + 1 | 0 : $11; + $7 = $3; + $3 = $9 + $3 | 0; + $9 = $11; + $26 = $3; + $7 = $3 >>> 0 < $7 >>> 0 ? $9 + 1 | 0 : $9; + $3 = $6; + $24 = ($3 & 131071) << 15 | $5 >>> 17; + $20 = __wasm_i64_mul($8, $21, $24, 0); + $3 = i64toi32_i32$HIGH_BITS; + $23 = $3; + $10 = $5; + $18 = $10 << 15 & -32768; + $11 = __wasm_i64_mul($22, $17, $18, 0); + $9 = $11 + $20 | 0; + $10 = i64toi32_i32$HIGH_BITS + $3 | 0; + $10 = $9 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; + $3 = $10; + $25 = __wasm_i64_mul($8, $21, $18, $25); + $18 = 0 + $25 | 0; + $10 = $9 + i64toi32_i32$HIGH_BITS | 0; + $10 = $18 >>> 0 < $25 >>> 0 ? $10 + 1 | 0 : $10; + $10 = ($9 | 0) == ($10 | 0) & $18 >>> 0 < $30 >>> 0 | $10 >>> 0 < $9 >>> 0; + $9 = ($3 | 0) == ($23 | 0) & $9 >>> 0 < $20 >>> 0 | $3 >>> 0 < $23 >>> 0; + $12 = $3; + $3 = __wasm_i64_mul($22, $17, $24, $27) + $3 | 0; + $11 = $9 + i64toi32_i32$HIGH_BITS | 0; + $11 = $3 >>> 0 < $12 >>> 0 ? $11 + 1 | 0 : $11; + $9 = $3; + $3 = $10 + $9 | 0; + $12 = $3 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; + $10 = $3; + $3 = $15 + $3 | 0; + $9 = $12 + $4 | 0; + $9 = $3 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; + $19 = $3; + $10 = $7; + $20 = $9; + $3 = ($4 | 0) == ($9 | 0) & $3 >>> 0 < $15 >>> 0 | $9 >>> 0 < $4 >>> 0; + $4 = $3 + $26 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $10 = $10 + 1 | 0 + } + $9 = $10; + $3 = ($19 | 0) != 0 | ($20 | 0) != 0; + $4 = $4 + $3 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $9 = $9 + 1 | 0 + } + $10 = $4; + $4 = 0 - $10 | 0; + $15 = 0; + $7 = __wasm_i64_mul($4, $15, $8, $21); + $3 = i64toi32_i32$HIGH_BITS; + $23 = $3; + $18 = __wasm_i64_mul($22, $17, $4, $15); + $4 = i64toi32_i32$HIGH_BITS; + $26 = $4; + $24 = 0 - ((0 < $10 >>> 0) + $9 | 0) | 0; + $9 = 0; + $15 = __wasm_i64_mul($8, $21, $24, $9); + $12 = $15 + $18 | 0; + $10 = i64toi32_i32$HIGH_BITS + $4 | 0; + $10 = $12 >>> 0 < $15 >>> 0 ? $10 + 1 | 0 : $10; + $4 = $12; + $15 = 0 + $7 | 0; + $11 = $3 + $4 | 0; + $11 = $15 >>> 0 < $27 >>> 0 ? $11 + 1 | 0 : $11; + $12 = $15; + $3 = $11; + $11 = ($23 | 0) == ($3 | 0) & $12 >>> 0 < $7 >>> 0 | $3 >>> 0 < $23 >>> 0; + $12 = ($10 | 0) == ($26 | 0) & $4 >>> 0 < $18 >>> 0 | $10 >>> 0 < $26 >>> 0; + $4 = __wasm_i64_mul($22, $17, $24, $9) + $10 | 0; + $9 = $12 + i64toi32_i32$HIGH_BITS | 0; + $9 = $4 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; + $7 = $4; + $4 = $11 + $4 | 0; + if ($4 >>> 0 < $7 >>> 0) { + $9 = $9 + 1 | 0 + } + $24 = $4; + $7 = $9; + $4 = 0 - $19 | 0; + $27 = 0 - ((0 < $19 >>> 0) + $20 | 0) | 0; + $19 = 0; + $26 = __wasm_i64_mul($27, $19, $8, $21); + $18 = i64toi32_i32$HIGH_BITS; + $20 = $4; + $25 = 0; + $9 = __wasm_i64_mul($4, $25, $22, $17); + $4 = $9 + $26 | 0; + $10 = i64toi32_i32$HIGH_BITS + $18 | 0; + $11 = $4; + $4 = $4 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; + $20 = __wasm_i64_mul($8, $21, $20, $25); + $8 = 0 + $20 | 0; + $9 = $11; + $10 = $9 + i64toi32_i32$HIGH_BITS | 0; + $10 = $8 >>> 0 < $20 >>> 0 ? $10 + 1 | 0 : $10; + $10 = ($9 | 0) == ($10 | 0) & $8 >>> 0 < $30 >>> 0 | $10 >>> 0 < $9 >>> 0; + $9 = ($4 | 0) == ($18 | 0) & $9 >>> 0 < $26 >>> 0 | $4 >>> 0 < $18 >>> 0; + $8 = $4; + $4 = __wasm_i64_mul($27, $19, $22, $17) + $4 | 0; + $12 = $9 + i64toi32_i32$HIGH_BITS | 0; + $12 = $4 >>> 0 < $8 >>> 0 ? $12 + 1 | 0 : $12; + $8 = $4; + $4 = $10 + $4 | 0; + $9 = $12; + $9 = $4 >>> 0 < $8 >>> 0 ? $9 + 1 | 0 : $9; + $8 = $4; + $4 = $15 + $4 | 0; + $9 = $9 + $3 | 0; + $9 = $4 >>> 0 < $8 >>> 0 ? $9 + 1 | 0 : $9; + $8 = $4; + $10 = $7; + $4 = $9; + $3 = ($3 | 0) == ($9 | 0) & $8 >>> 0 < $15 >>> 0 | $9 >>> 0 < $3 >>> 0; + $7 = $3 + $24 | 0; + if ($7 >>> 0 < $3 >>> 0) { + $10 = $10 + 1 | 0 + } + $3 = $7; + $9 = $10; + $12 = $3; + $11 = $4 + -1 | 0; + $3 = $8 + -2 | 0; + if ($3 >>> 0 < 4294967294) { + $11 = $11 + 1 | 0 + } + $7 = $3; + $10 = $3; + $3 = $11; + $4 = ($4 | 0) == ($3 | 0) & $10 >>> 0 < $8 >>> 0 | $3 >>> 0 < $4 >>> 0; + $8 = $12 + $4 | 0; + if ($8 >>> 0 < $4 >>> 0) { + $9 = $9 + 1 | 0 + } + $4 = $8 + -1 | 0; + $10 = $9 + -1 | 0; + $10 = ($4 | 0) != -1 ? $10 + 1 | 0 : $10; + $8 = 0; + $22 = $8; + $17 = $4; + $9 = $16; + $18 = $9 << 2 | $2 >>> 30; + $24 = 0; + $12 = __wasm_i64_mul($4, $8, $18, $24); + $8 = i64toi32_i32$HIGH_BITS; + $15 = $8; + $11 = $8; + $8 = $2; + $27 = ($8 & 1073741823) << 2 | $1 >>> 30; + $25 = $10; + $8 = 0; + $9 = __wasm_i64_mul($27, 0, $10, $8); + $4 = $9 + $12 | 0; + $11 = i64toi32_i32$HIGH_BITS + $11 | 0; + $11 = $4 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; + $9 = $4; + $20 = $11; + $23 = ($15 | 0) == ($11 | 0) & $9 >>> 0 < $12 >>> 0 | $11 >>> 0 < $15 >>> 0; + $12 = $11; + $11 = 0; + $15 = $11; + $10 = 0; + $26 = $3; + $30 = (($14 & 1073741823) << 2 | $16 >>> 30) & -262145 | 262144; + $4 = __wasm_i64_mul($3, $11, $30, 0); + $3 = $4 + $9 | 0; + $12 = i64toi32_i32$HIGH_BITS + $12 | 0; + $12 = $3 >>> 0 < $4 >>> 0 ? $12 + 1 | 0 : $12; + $16 = $3; + $4 = $12; + $3 = ($20 | 0) == ($4 | 0) & $3 >>> 0 < $9 >>> 0 | $4 >>> 0 < $20 >>> 0; + $9 = $3 + $23 | 0; + if ($9 >>> 0 < $3 >>> 0) { + $10 = 1 + } + $11 = __wasm_i64_mul($25, $8, $30, $35); + $3 = $11 + $9 | 0; + $9 = i64toi32_i32$HIGH_BITS + $10 | 0; + $10 = $3 >>> 0 < $11 >>> 0 ? $9 + 1 | 0 : $9; + $11 = __wasm_i64_mul($17, $22, $30, $35); + $9 = i64toi32_i32$HIGH_BITS; + $2 = $3; + $14 = __wasm_i64_mul($18, $24, $25, $8); + $3 = $14 + $11 | 0; + $12 = i64toi32_i32$HIGH_BITS + $9 | 0; + $12 = $3 >>> 0 < $14 >>> 0 ? $12 + 1 | 0 : $12; + $14 = $3; + $3 = $12; + $12 = ($9 | 0) == ($3 | 0) & $14 >>> 0 < $11 >>> 0 | $3 >>> 0 < $9 >>> 0; + $11 = $2 + $3 | 0; + $10 = $10 + $12 | 0; + $9 = $11; + $12 = $9 >>> 0 < $3 >>> 0 ? $10 + 1 | 0 : $10; + $2 = $9; + $11 = $4 + $14 | 0; + $10 = 0; + $3 = $10 + $16 | 0; + if ($3 >>> 0 < $10 >>> 0) { + $11 = $11 + 1 | 0 + } + $14 = $3; + $9 = $3; + $3 = $11; + $4 = ($4 | 0) == ($3 | 0) & $9 >>> 0 < $16 >>> 0 | $3 >>> 0 < $4 >>> 0; + $9 = $2 + $4 | 0; + if ($9 >>> 0 < $4 >>> 0) { + $12 = $12 + 1 | 0 + } + $39 = $9; + $4 = $14; + $10 = $3; + $16 = __wasm_i64_mul($27, $19, $26, $15); + $11 = i64toi32_i32$HIGH_BITS; + $20 = $7; + $23 = __wasm_i64_mul($7, 0, $18, $24); + $7 = $23 + $16 | 0; + $9 = i64toi32_i32$HIGH_BITS + $11 | 0; + $9 = $7 >>> 0 < $23 >>> 0 ? $9 + 1 | 0 : $9; + $21 = $7; + $7 = $9; + $16 = ($11 | 0) == ($9 | 0) & $21 >>> 0 < $16 >>> 0 | $9 >>> 0 < $11 >>> 0; + $11 = $9; + $40 = $4; + $9 = 0; + $41 = $16; + $36 = $1 << 2 & -4; + $2 = 0; + $16 = __wasm_i64_mul($17, $22, $36, $2); + $4 = $16 + $21 | 0; + $11 = i64toi32_i32$HIGH_BITS + $11 | 0; + $11 = $4 >>> 0 < $16 >>> 0 ? $11 + 1 | 0 : $11; + $23 = $4; + $16 = $4; + $4 = $11; + $7 = ($7 | 0) == ($4 | 0) & $16 >>> 0 < $21 >>> 0 | $4 >>> 0 < $7 >>> 0; + $11 = $41 + $7 | 0; + if ($11 >>> 0 < $7 >>> 0) { + $9 = 1 + } + $7 = $40 + $11 | 0; + $10 = $9 + $10 | 0; + $10 = $7 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; + $16 = $7; + $11 = $12; + $7 = $10; + $3 = ($3 | 0) == ($10 | 0) & $16 >>> 0 < $14 >>> 0 | $10 >>> 0 < $3 >>> 0; + $9 = $3 + $39 | 0; + if ($9 >>> 0 < $3 >>> 0) { + $11 = $11 + 1 | 0 + } + $40 = $9; + $14 = $16; + $21 = $7; + $39 = __wasm_i64_mul($25, $8, $36, $2); + $25 = i64toi32_i32$HIGH_BITS; + $8 = __wasm_i64_mul($30, $35, $20, $37); + $3 = $8 + $39 | 0; + $12 = i64toi32_i32$HIGH_BITS + $25 | 0; + $12 = $3 >>> 0 < $8 >>> 0 ? $12 + 1 | 0 : $12; + $30 = $3; + $9 = __wasm_i64_mul($18, $24, $26, $15); + $3 = $3 + $9 | 0; + $8 = $12; + $10 = $8 + i64toi32_i32$HIGH_BITS | 0; + $10 = $3 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; + $18 = $3; + $12 = __wasm_i64_mul($17, $22, $27, $19); + $3 = $3 + $12 | 0; + $9 = i64toi32_i32$HIGH_BITS + $10 | 0; + $17 = $3; + $9 = $3 >>> 0 < $12 >>> 0 ? $9 + 1 | 0 : $9; + $22 = 0; + $12 = $11; + $3 = $9; + $9 = ($9 | 0) == ($10 | 0) & $17 >>> 0 < $18 >>> 0 | $9 >>> 0 < $10 >>> 0; + $11 = ($8 | 0) == ($25 | 0) & $30 >>> 0 < $39 >>> 0 | $8 >>> 0 < $25 >>> 0; + $8 = ($8 | 0) == ($10 | 0) & $18 >>> 0 < $30 >>> 0 | $10 >>> 0 < $8 >>> 0; + $10 = $11 + $8 | 0; + $10 >>> 0 < $8 >>> 0; + $8 = $9 + $10 | 0; + $10 = $8; + $9 = $3 | 0; + $8 = $9 + $14 | 0; + $10 = ($10 | $22) + $21 | 0; + $10 = $8 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; + $21 = $8; + $14 = $10; + $7 = ($7 | 0) == ($10 | 0) & $8 >>> 0 < $16 >>> 0 | $10 >>> 0 < $7 >>> 0; + $8 = $7 + $40 | 0; + if ($8 >>> 0 < $7 >>> 0) { + $12 = $12 + 1 | 0 + } + $24 = $8; + $8 = $12; + $12 = $21; + $16 = $14; + $22 = $23; + $26 = __wasm_i64_mul($26, $15, $36, $2); + $15 = i64toi32_i32$HIGH_BITS; + $9 = __wasm_i64_mul($27, $19, $20, $37); + $7 = $9 + $26 | 0; + $11 = i64toi32_i32$HIGH_BITS + $15 | 0; + $11 = $7 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; + $10 = $11; + $19 = $10; + $11 = 0; + $9 = ($10 | 0) == ($15 | 0) & $7 >>> 0 < $26 >>> 0 | $10 >>> 0 < $15 >>> 0; + $7 = $10 + $22 | 0; + $10 = ($9 | $11) + $4 | 0; + $10 = $7 >>> 0 < $19 >>> 0 ? $10 + 1 | 0 : $10; + $19 = $7; + $9 = $7; + $7 = $10; + $9 = ($4 | 0) == ($10 | 0) & $9 >>> 0 < $22 >>> 0 | $10 >>> 0 < $4 >>> 0; + $23 = $12; + $4 = $9; + $9 = $10 + $17 | 0; + $12 = 0; + $3 = $12 + $19 | 0; + if ($3 >>> 0 < $12 >>> 0) { + $9 = $9 + 1 | 0 + } + $3 = ($7 | 0) == ($9 | 0) & $3 >>> 0 < $19 >>> 0 | $9 >>> 0 < $7 >>> 0; + $4 = $4 + $3 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $11 = 1 + } + $3 = $23 + $4 | 0; + $12 = $11 + $16 | 0; + $7 = $3; + $9 = $8; + $12 = $3 >>> 0 < $4 >>> 0 ? $12 + 1 | 0 : $12; + $8 = $12; + $3 = ($14 | 0) == ($8 | 0) & $3 >>> 0 < $21 >>> 0 | $8 >>> 0 < $14 >>> 0; + $4 = $3 + $24 | 0; + if ($4 >>> 0 < $3 >>> 0) { + $9 = $9 + 1 | 0 + } + $3 = $4; + $4 = $9; + label$12 : { + if (($9 | 0) == 131071 | $9 >>> 0 < 131071) { + $22 = 0; + $14 = $5; + $18 = 0; + $10 = __wasm_i64_mul($7, $22, $14, $18); + $11 = i64toi32_i32$HIGH_BITS; + $9 = $1 << 17; + $1 = 0; + $2 = ($10 | 0) != 0 | ($11 | 0) != 0; + $16 = $1 - $2 | 0; + $30 = $9 - ($1 >>> 0 < $2 >>> 0) | 0; + $19 = 0 - $10 | 0; + $15 = 0 - ((0 < $10 >>> 0) + $11 | 0) | 0; + $2 = 0; + $24 = __wasm_i64_mul($8, $2, $14, $18); + $1 = i64toi32_i32$HIGH_BITS; + $27 = $1; + $17 = 0; + $10 = __wasm_i64_mul($7, $22, $6, $17); + $9 = $10 + $24 | 0; + $11 = i64toi32_i32$HIGH_BITS + $1 | 0; + $11 = $9 >>> 0 < $10 >>> 0 ? $11 + 1 | 0 : $11; + $1 = $9; + $10 = $9; + $20 = 0; + $9 = $20; + $23 = $10; + $9 = ($10 | 0) == ($15 | 0) & $19 >>> 0 < $9 >>> 0 | $15 >>> 0 < $10 >>> 0; + $21 = $16 - $9 | 0; + $30 = $30 - ($16 >>> 0 < $9 >>> 0) | 0; + $9 = __wasm_i64_mul($3, 0, $14, $18); + $10 = i64toi32_i32$HIGH_BITS; + $14 = __wasm_i64_mul($7, $22, $29, 0); + $9 = $14 + $9 | 0; + $12 = i64toi32_i32$HIGH_BITS + $10 | 0; + $12 = $9 >>> 0 < $14 >>> 0 ? $12 + 1 | 0 : $12; + $14 = __wasm_i64_mul($8, $2, $6, $17); + $9 = $14 + $9 | 0; + $10 = i64toi32_i32$HIGH_BITS + $12 | 0; + $10 = $9 >>> 0 < $14 >>> 0 ? $10 + 1 | 0 : $10; + $12 = $10; + $10 = ($11 | 0) == ($27 | 0) & $1 >>> 0 < $24 >>> 0 | $11 >>> 0 < $27 >>> 0; + $1 = $11 + $9 | 0; + $10 = $10 + $12 | 0; + $10 = $1 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; + $11 = $1; + $1 = $10; + $9 = __wasm_i64_mul($7, $8, $31, 0); + $14 = i64toi32_i32$HIGH_BITS; + $16 = $11; + $11 = __wasm_i64_mul($5, $6, $4, 0); + $10 = $11 + $9 | 0; + $9 = i64toi32_i32$HIGH_BITS + $14 | 0; + $9 = $10 >>> 0 < $11 >>> 0 ? $9 + 1 | 0 : $9; + $12 = __wasm_i64_mul($3, $4, $6, $17); + $11 = $12 + $10 | 0; + $9 = __wasm_i64_mul($8, $2, $29, $32); + $2 = $9 + $11 | 0; + $9 = $2; + $10 = 0; + $2 = $16 + $10 | 0; + $9 = $1 + $9 | 0; + $1 = $2; + $16 = $21 - $1 | 0; + $2 = $30 - (($21 >>> 0 < $1 >>> 0) + ($1 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9) | 0) | 0; + $34 = $34 + -1 | 0; + $29 = $19 - $20 | 0; + $1 = $15 - (($19 >>> 0 < $20 >>> 0) + $23 | 0) | 0; + break label$12; + } + $17 = $8 >>> 1 | 0; + $11 = 0; + $12 = $1 << 16; + $10 = $3 << 31; + $7 = ($8 & 1) << 31 | $7 >>> 1; + $8 = $8 >>> 1 | $10; + $27 = 0; + $25 = 0; + $1 = __wasm_i64_mul($7, $27, $5, $25); + $9 = i64toi32_i32$HIGH_BITS; + $10 = $9; + $9 = ($1 | 0) != 0 | ($9 | 0) != 0; + $14 = $2 - $9 | 0; + $37 = $12 - ($2 >>> 0 < $9 >>> 0) | 0; + $21 = 0 - $1 | 0; + $22 = 0 - ((0 < $1 >>> 0) + $10 | 0) | 0; + $12 = $22; + $15 = 0; + $20 = __wasm_i64_mul($7, $27, $6, $15); + $1 = i64toi32_i32$HIGH_BITS; + $35 = $1; + $23 = $17 | $3 << 31; + $36 = $4 << 31 | $3 >>> 1 | $11; + $10 = $23; + $17 = __wasm_i64_mul($10, 0, $5, $25); + $2 = $17 + $20 | 0; + $9 = i64toi32_i32$HIGH_BITS + $1 | 0; + $9 = $2 >>> 0 < $17 >>> 0 ? $9 + 1 | 0 : $9; + $1 = $9; + $9 = $2; + $26 = $9; + $18 = 0; + $9 = ($9 | 0) == ($12 | 0) & $21 >>> 0 < $18 >>> 0 | $12 >>> 0 < $9 >>> 0; + $24 = $14 - $9 | 0; + $37 = $37 - ($14 >>> 0 < $9 >>> 0) | 0; + $10 = __wasm_i64_mul($6, $15, $10, $11); + $11 = i64toi32_i32$HIGH_BITS; + $9 = $4; + $12 = $9 >>> 1 | 0; + $17 = ($9 & 1) << 31 | $3 >>> 1; + $14 = $12; + $12 = __wasm_i64_mul($17, 0, $5, $25); + $9 = $12 + $10 | 0; + $10 = i64toi32_i32$HIGH_BITS + $11 | 0; + $10 = $9 >>> 0 < $12 >>> 0 ? $10 + 1 | 0 : $10; + $12 = __wasm_i64_mul($7, $27, $29, 0); + $11 = $12 + $9 | 0; + $9 = i64toi32_i32$HIGH_BITS + $10 | 0; + $10 = $11; + $11 = $10 >>> 0 < $12 >>> 0 ? $9 + 1 | 0 : $9; + $9 = ($1 | 0) == ($35 | 0) & $2 >>> 0 < $20 >>> 0 | $1 >>> 0 < $35 >>> 0; + $2 = $1; + $1 = $1 + $10 | 0; + $11 = $9 + $11 | 0; + $9 = $1; + $1 = $9 >>> 0 < $2 >>> 0 ? $11 + 1 | 0 : $11; + $2 = __wasm_i64_mul($7, $8, $31, 0); + $10 = i64toi32_i32$HIGH_BITS; + $11 = $9; + $3 = __wasm_i64_mul($5, $6, $4 >>> 1 | 0, 0); + $2 = $3 + $2 | 0; + $9 = i64toi32_i32$HIGH_BITS + $10 | 0; + $9 = $2 >>> 0 < $3 >>> 0 ? $9 + 1 | 0 : $9; + $3 = __wasm_i64_mul($6, $15, $17, $14); + $2 = $3 + $2 | 0; + $9 = i64toi32_i32$HIGH_BITS + $9 | 0; + $3 = __wasm_i64_mul($23, $36, $29, $32); + $2 = $3 + $2 | 0; + $9 = $2; + $3 = 0; + $2 = $11 + $3 | 0; + $10 = $1 + $9 | 0; + $1 = $2; + $16 = $24 - $1 | 0; + $2 = $37 - (($24 >>> 0 < $1 >>> 0) + ($1 >>> 0 < $3 >>> 0 ? $10 + 1 | 0 : $10) | 0) | 0; + $3 = $17; + $4 = $14; + $29 = $21 - $18 | 0; + $1 = $22 - (($21 >>> 0 < $18 >>> 0) + $26 | 0) | 0; + } + if (($34 | 0) >= 16384) { + $28 = $28 | 2147418112; + $1 = 0; + $2 = 0; + break label$2; + } + $11 = $34 + 16383 | 0; + if (($34 | 0) <= -16383) { + label$16 : { + if ($11) { + break label$16 + } + $11 = $8; + $14 = $29; + $12 = $1 << 1 | $14 >>> 31; + $9 = $14 << 1; + $6 = ($6 | 0) == ($12 | 0) & $9 >>> 0 > $5 >>> 0 | $12 >>> 0 > $6 >>> 0; + $9 = $4 & 65535; + $5 = $16; + $12 = $2 << 1 | $5 >>> 31; + $2 = $5 << 1 | $1 >>> 31; + $4 = $2; + $1 = $12; + $1 = ($4 | 0) == ($38 | 0) & ($1 | 0) == ($31 | 0) ? $6 : ($31 | 0) == ($1 | 0) & $4 >>> 0 > $38 >>> 0 | $1 >>> 0 > $31 >>> 0; + $2 = $1 + $7 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $11 = $11 + 1 | 0 + } + $1 = $2; + $4 = $1; + $2 = $11; + $4 = $3 + (($8 | 0) == ($11 | 0) & $4 >>> 0 < $7 >>> 0 | $11 >>> 0 < $8 >>> 0) | 0; + if ($4 >>> 0 < $3 >>> 0) { + $9 = $9 + 1 | 0 + } + $3 = $9; + if (!($9 & 65536)) { + break label$16 + } + $33 = $4 | $33; + $28 = $3 | $28; + break label$2; + } + $1 = 0; + $2 = 0; + break label$2; + } + $10 = $8; + $4 = $4 & 65535; + $14 = $29; + $9 = $1 << 1 | $14 >>> 31; + $14 = $14 << 1; + $6 = ($6 | 0) == ($9 | 0) & $14 >>> 0 >= $5 >>> 0 | $9 >>> 0 > $6 >>> 0; + $5 = $16; + $9 = $2 << 1 | $5 >>> 31; + $2 = $5 << 1 | $1 >>> 31; + $1 = ($2 | 0) == ($38 | 0) & ($9 | 0) == ($31 | 0) ? $6 : ($31 | 0) == ($9 | 0) & $2 >>> 0 >= $38 >>> 0 | $9 >>> 0 > $31 >>> 0; + $2 = $1 + $7 | 0; + if ($2 >>> 0 < $1 >>> 0) { + $10 = $10 + 1 | 0 + } + $1 = $2; + $2 = $10; + $5 = $3; + $3 = (($8 | 0) == ($10 | 0) & $1 >>> 0 < $7 >>> 0 | $10 >>> 0 < $8 >>> 0) + $3 | 0; + $10 = $11 << 16 | $4; + $33 = $3 | $33; + $28 = $28 | ($3 >>> 0 < $5 >>> 0 ? $10 + 1 | 0 : $10); + } + HEAP32[$0 >> 2] = $1; + HEAP32[$0 + 4 >> 2] = $2; + HEAP32[$0 + 8 >> 2] = $33; + HEAP32[$0 + 12 >> 2] = $28; + global$0 = $13 + 192 | 0; + return; + } + HEAP32[$0 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + $1 = !($3 | $5 | ($4 | $6)); + HEAP32[$0 + 8 >> 2] = $1 ? 0 : $33; + HEAP32[$0 + 12 >> 2] = $1 ? 2147450880 : $28; + global$0 = $13 + 192 | 0; + } + + function __fpclassifyl($0, $1, $2, $3) { + var $4 = 0, $5 = 0; + $5 = $3 & 65535; + $3 = $3 >>> 16 & 32767; + label$1 : { + if (($3 | 0) != 32767) { + $4 = 4; + if ($3) { + break label$1 + } + return $0 | $2 | ($1 | $5) ? 3 : 2; + } + $4 = !($0 | $2 | ($1 | $5)); + } + return $4; + } + + function fmodl($0, $1, $2, $3, $4, $5, $6, $7, $8) { + var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0; + $9 = global$0 - 128 | 0; + global$0 = $9; + label$1 : { + label$2 : { + label$3 : { + if (!__letf2($5, $6, $7, $8, 0, 0, 0, 0)) { + break label$3 + } + $10 = __fpclassifyl($5, $6, $7, $8); + $19 = $4 >>> 16 | 0; + $14 = $19 & 32767; + if (($14 | 0) == 32767) { + break label$3 + } + if ($10) { + break label$2 + } + } + __multf3($9 + 16 | 0, $1, $2, $3, $4, $5, $6, $7, $8); + $4 = HEAP32[$9 + 16 >> 2]; + $3 = HEAP32[$9 + 20 >> 2]; + $2 = HEAP32[$9 + 24 >> 2]; + $1 = HEAP32[$9 + 28 >> 2]; + __divtf3($9, $4, $3, $2, $1, $4, $3, $2, $1); + $3 = HEAP32[$9 + 8 >> 2]; + $4 = HEAP32[$9 + 12 >> 2]; + $7 = HEAP32[$9 >> 2]; + $8 = HEAP32[$9 + 4 >> 2]; + break label$1; + } + $11 = $4 & 65535 | $14 << 16; + $12 = $11; + $13 = $3; + $15 = $7; + $18 = $8 >>> 16 & 32767; + $10 = $8 & 65535 | $18 << 16; + if ((__letf2($1, $2, $13, $12, $5, $6, $7, $10) | 0) <= 0) { + if (__letf2($1, $2, $13, $12, $5, $6, $15, $10)) { + $7 = $1; + $8 = $2; + break label$1; + } + __multf3($9 + 112 | 0, $1, $2, $3, $4, 0, 0, 0, 0); + $3 = HEAP32[$9 + 120 >> 2]; + $4 = HEAP32[$9 + 124 >> 2]; + $7 = HEAP32[$9 + 112 >> 2]; + $8 = HEAP32[$9 + 116 >> 2]; + break label$1; + } + if ($14) { + $8 = $2; + $7 = $1; + } else { + __multf3($9 + 96 | 0, $1, $2, $13, $12, 0, 0, 0, 1081540608); + $7 = HEAP32[$9 + 108 >> 2]; + $12 = $7; + $13 = HEAP32[$9 + 104 >> 2]; + $14 = ($7 >>> 16 | 0) + -120 | 0; + $8 = HEAP32[$9 + 100 >> 2]; + $7 = HEAP32[$9 + 96 >> 2]; + } + if (!$18) { + __multf3($9 + 80 | 0, $5, $6, $15, $10, 0, 0, 0, 1081540608); + $5 = HEAP32[$9 + 92 >> 2]; + $10 = $5; + $15 = HEAP32[$9 + 88 >> 2]; + $18 = ($10 >>> 16 | 0) + -120 | 0; + $6 = HEAP32[$9 + 84 >> 2]; + $5 = HEAP32[$9 + 80 >> 2]; + } + $21 = $15; + $11 = $15; + $15 = $13 - $11 | 0; + $12 = $12 & 65535 | 65536; + $20 = $10 & 65535 | 65536; + $10 = ($6 | 0) == ($8 | 0) & $7 >>> 0 < $5 >>> 0 | $8 >>> 0 < $6 >>> 0; + $11 = ($12 - ($20 + ($13 >>> 0 < $11 >>> 0) | 0) | 0) - ($15 >>> 0 < $10 >>> 0) | 0; + $17 = $15 - $10 | 0; + $16 = ($11 | 0) > -1 ? 1 : 0; + $15 = $7 - $5 | 0; + $10 = $8 - (($7 >>> 0 < $5 >>> 0) + $6 | 0) | 0; + if (($14 | 0) > ($18 | 0)) { + while (1) { + label$11 : { + if ($16 & 1) { + if (!($15 | $17 | ($10 | $11))) { + __multf3($9 + 32 | 0, $1, $2, $3, $4, 0, 0, 0, 0); + $3 = HEAP32[$9 + 40 >> 2]; + $4 = HEAP32[$9 + 44 >> 2]; + $7 = HEAP32[$9 + 32 >> 2]; + $8 = HEAP32[$9 + 36 >> 2]; + break label$1; + } + $7 = $17; + $16 = $11 << 1 | $7 >>> 31; + $17 = $7 << 1; + $11 = $16; + $16 = 0; + $7 = $10 >>> 31 | 0; + break label$11; + } + $11 = 0; + $10 = $8; + $17 = $8 >>> 31 | 0; + $15 = $7; + $7 = $13; + $16 = $12 << 1 | $7 >>> 31; + $7 = $7 << 1; + } + $13 = $7 | $17; + $8 = $13; + $7 = $21; + $17 = $8 - $7 | 0; + $12 = $11 | $16; + $11 = $12 - (($8 >>> 0 < $7 >>> 0) + $20 | 0) | 0; + $7 = $15; + $16 = $10 << 1 | $7 >>> 31; + $7 = $7 << 1; + $8 = $16; + $10 = ($6 | 0) == ($8 | 0) & $7 >>> 0 < $5 >>> 0 | $8 >>> 0 < $6 >>> 0; + $11 = $11 - ($17 >>> 0 < $10 >>> 0) | 0; + $17 = $17 - $10 | 0; + $16 = ($11 | 0) > -1 ? 1 : 0; + $15 = $7 - $5 | 0; + $10 = $8 - (($7 >>> 0 < $5 >>> 0) + $6 | 0) | 0; + $14 = $14 + -1 | 0; + if (($14 | 0) > ($18 | 0)) { + continue + } + break; + }; + $14 = $18; + } + label$14 : { + if (!$16) { + break label$14 + } + $7 = $15; + $13 = $17; + $8 = $10; + $12 = $11; + if ($7 | $13 | ($8 | $12)) { + break label$14 + } + __multf3($9 + 48 | 0, $1, $2, $3, $4, 0, 0, 0, 0); + $3 = HEAP32[$9 + 56 >> 2]; + $4 = HEAP32[$9 + 60 >> 2]; + $7 = HEAP32[$9 + 48 >> 2]; + $8 = HEAP32[$9 + 52 >> 2]; + break label$1; + } + if (($12 | 0) == 65535 | $12 >>> 0 < 65535) { + while (1) { + $3 = $8 >>> 31 | 0; + $1 = 0; + $14 = $14 + -1 | 0; + $11 = $8 << 1 | $7 >>> 31; + $7 = $7 << 1; + $8 = $11; + $2 = $13; + $16 = $12 << 1 | $2 >>> 31; + $13 = $2 << 1 | $3; + $1 = $1 | $16; + $12 = $1; + if (($1 | 0) == 65536 & $13 >>> 0 < 0 | $1 >>> 0 < 65536) { + continue + } + break; + } + } + $1 = $19 & 32768; + if (($14 | 0) <= 0) { + __multf3($9 - -64 | 0, $7, $8, $13, $12 & 65535 | ($1 | $14 + 120) << 16, 0, 0, 0, 1065811968); + $3 = HEAP32[$9 + 72 >> 2]; + $4 = HEAP32[$9 + 76 >> 2]; + $7 = HEAP32[$9 + 64 >> 2]; + $8 = HEAP32[$9 + 68 >> 2]; + break label$1; + } + $3 = $13; + $4 = $12 & 65535 | ($1 | $14) << 16; + } + HEAP32[$0 >> 2] = $7; + HEAP32[$0 + 4 >> 2] = $8; + HEAP32[$0 + 8 >> 2] = $3; + HEAP32[$0 + 12 >> 2] = $4; + global$0 = $9 + 128 | 0; + } + + function __floatscan($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; + $5 = global$0 - 48 | 0; + global$0 = $5; + $4 = $1 + 4 | 0; + $7 = HEAP32[2644]; + $10 = HEAP32[2641]; + while (1) { + $2 = HEAP32[$1 + 4 >> 2]; + label$4 : { + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$4; + } + $2 = __shgetc($1); + } + if (($2 | 0) == 32 | $2 + -9 >>> 0 < 5) { + continue + } + break; + }; + $6 = 1; + label$6 : { + label$7 : { + switch ($2 + -43 | 0) { + case 0: + case 2: + break label$7; + default: + break label$6; + }; + } + $6 = ($2 | 0) == 45 ? -1 : 1; + $2 = HEAP32[$1 + 4 >> 2]; + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$6; + } + $2 = __shgetc($1); + } + label$1 : { + label$9 : { + label$10 : { + while (1) { + if (HEAP8[$3 + 10484 | 0] == ($2 | 32)) { + label$13 : { + if ($3 >>> 0 > 6) { + break label$13 + } + $2 = HEAP32[$1 + 4 >> 2]; + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$13; + } + $2 = __shgetc($1); + } + $3 = $3 + 1 | 0; + if (($3 | 0) != 8) { + continue + } + break label$10; + } + break; + }; + if (($3 | 0) != 3) { + if (($3 | 0) == 8) { + break label$10 + } + if ($3 >>> 0 < 4) { + break label$9 + } + if (($3 | 0) == 8) { + break label$10 + } + } + $1 = HEAP32[$1 + 104 >> 2]; + if ($1) { + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 + } + if ($3 >>> 0 < 4) { + break label$10 + } + while (1) { + if ($1) { + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 + } + $3 = $3 + -1 | 0; + if ($3 >>> 0 > 3) { + continue + } + break; + }; + } + __extendsftf2($5, Math_fround(Math_fround($6 | 0) * Math_fround(infinity))); + $6 = HEAP32[$5 + 8 >> 2]; + $2 = HEAP32[$5 + 12 >> 2]; + $8 = HEAP32[$5 >> 2]; + $9 = HEAP32[$5 + 4 >> 2]; + break label$1; + } + label$19 : { + label$20 : { + label$21 : { + if ($3) { + break label$21 + } + $3 = 0; + while (1) { + if (HEAP8[$3 + 10493 | 0] != ($2 | 32)) { + break label$21 + } + label$23 : { + if ($3 >>> 0 > 1) { + break label$23 + } + $2 = HEAP32[$1 + 4 >> 2]; + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$23; + } + $2 = __shgetc($1); + } + $3 = $3 + 1 | 0; + if (($3 | 0) != 3) { + continue + } + break; + }; + break label$20; + } + label$25 : { + switch ($3 | 0) { + case 0: + label$27 : { + if (($2 | 0) != 48) { + break label$27 + } + $3 = HEAP32[$1 + 4 >> 2]; + label$28 : { + if ($3 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $3 + 1; + $3 = HEAPU8[$3 | 0]; + break label$28; + } + $3 = __shgetc($1); + } + if (($3 & -33) == 88) { + hexfloat($5 + 16 | 0, $1, $10, $7, $6); + $6 = HEAP32[$5 + 24 >> 2]; + $2 = HEAP32[$5 + 28 >> 2]; + $8 = HEAP32[$5 + 16 >> 2]; + $9 = HEAP32[$5 + 20 >> 2]; + break label$1; + } + if (!HEAP32[$1 + 104 >> 2]) { + break label$27 + } + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1; + } + decfloat($5 + 32 | 0, $1, $2, $10, $7, $6); + $6 = HEAP32[$5 + 40 >> 2]; + $2 = HEAP32[$5 + 44 >> 2]; + $8 = HEAP32[$5 + 32 >> 2]; + $9 = HEAP32[$5 + 36 >> 2]; + break label$1; + case 3: + break label$20; + default: + break label$25; + }; + } + if (HEAP32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 + } + break label$19; + } + label$32 : { + $3 = HEAP32[$1 + 4 >> 2]; + label$33 : { + if ($3 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $3 + 1; + $2 = HEAPU8[$3 | 0]; + break label$33; + } + $2 = __shgetc($1); + } + if (($2 | 0) == 40) { + $3 = 1; + break label$32; + } + $6 = 0; + $2 = 2147450880; + if (!HEAP32[$1 + 104 >> 2]) { + break label$1 + } + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1; + break label$1; + } + while (1) { + $2 = HEAP32[$1 + 4 >> 2]; + label$37 : { + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$4 >> 2] = $2 + 1; + $7 = HEAPU8[$2 | 0]; + break label$37; + } + $7 = __shgetc($1); + } + if (!($7 + -97 >>> 0 >= 26 ? !($7 + -48 >>> 0 < 10 | $7 + -65 >>> 0 < 26 | ($7 | 0) == 95) : 0)) { + $3 = $3 + 1 | 0; + continue; + } + break; + }; + $6 = 0; + $2 = 2147450880; + if (($7 | 0) == 41) { + break label$1 + } + $1 = HEAP32[$1 + 104 >> 2]; + if ($1) { + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 + } + if (!$3) { + break label$1 + } + while (1) { + $3 = $3 + -1 | 0; + if ($1) { + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 + } + if ($3) { + continue + } + break; + }; + break label$1; + } + HEAP32[2896] = 28; + __shlim($1); + $6 = 0; + $2 = 0; + } + HEAP32[$0 >> 2] = $8; + HEAP32[$0 + 4 >> 2] = $9; + HEAP32[$0 + 8 >> 2] = $6; + HEAP32[$0 + 12 >> 2] = $2; + global$0 = $5 + 48 | 0; + } + + function hexfloat($0, $1, $2, $3, $4) { + var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0; + $5 = global$0 - 432 | 0; + global$0 = $5; + $6 = HEAP32[$1 + 4 >> 2]; + label$1 : { + if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $6 + 1; + $7 = HEAPU8[$6 | 0]; + break label$1; + } + $7 = __shgetc($1); + } + label$3 : { + label$4 : { + while (1) { + if (($7 | 0) != 48) { + label$6 : { + if (($7 | 0) != 46) { + break label$3 + } + $6 = HEAP32[$1 + 4 >> 2]; + if ($6 >>> 0 >= HEAPU32[$1 + 104 >> 2]) { + break label$6 + } + HEAP32[$1 + 4 >> 2] = $6 + 1; + $7 = HEAPU8[$6 | 0]; + break label$4; + } + } else { + $6 = HEAP32[$1 + 4 >> 2]; + if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $6 + 1; + $7 = HEAPU8[$6 | 0]; + } else { + $7 = __shgetc($1) + } + $21 = 1; + continue; + } + break; + }; + $7 = __shgetc($1); + } + $20 = 1; + if (($7 | 0) != 48) { + break label$3 + } + while (1) { + $6 = HEAP32[$1 + 4 >> 2]; + label$10 : { + if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $6 + 1; + $7 = HEAPU8[$6 | 0]; + break label$10; + } + $7 = __shgetc($1); + } + $13 = $13 + -1 | 0; + $17 = $17 + -1 | 0; + if (($17 | 0) != -1) { + $13 = $13 + 1 | 0 + } + if (($7 | 0) == 48) { + continue + } + break; + }; + $21 = 1; + } + $12 = 1073676288; + $6 = 0; + while (1) { + label$13 : { + $22 = $7 | 32; + label$14 : { + label$15 : { + $23 = $7 + -48 | 0; + if ($23 >>> 0 < 10) { + break label$15 + } + if ($22 + -97 >>> 0 > 5 ? ($7 | 0) != 46 : 0) { + break label$13 + } + if (($7 | 0) != 46) { + break label$15 + } + if ($20) { + break label$13 + } + $20 = 1; + $17 = $9; + $13 = $6; + break label$14; + } + $7 = ($7 | 0) > 57 ? $22 + -87 | 0 : $23; + label$16 : { + if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 7 ? 0 : 1) : 0) { + $14 = $7 + ($14 << 4) | 0; + break label$16; + } + if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 28 ? 0 : 1) : 0) { + __floatsitf($5 + 48 | 0, $7); + __multf3($5 + 32 | 0, $18, $19, $8, $12, 0, 0, 0, 1073414144); + $18 = HEAP32[$5 + 32 >> 2]; + $19 = HEAP32[$5 + 36 >> 2]; + $8 = HEAP32[$5 + 40 >> 2]; + $12 = HEAP32[$5 + 44 >> 2]; + __multf3($5 + 16 | 0, $18, $19, $8, $12, HEAP32[$5 + 48 >> 2], HEAP32[$5 + 52 >> 2], HEAP32[$5 + 56 >> 2], HEAP32[$5 + 60 >> 2]); + __addtf3($5, $10, $11, $15, $16, HEAP32[$5 + 16 >> 2], HEAP32[$5 + 20 >> 2], HEAP32[$5 + 24 >> 2], HEAP32[$5 + 28 >> 2]); + $15 = HEAP32[$5 + 8 >> 2]; + $16 = HEAP32[$5 + 12 >> 2]; + $10 = HEAP32[$5 >> 2]; + $11 = HEAP32[$5 + 4 >> 2]; + break label$16; + } + if (!$7 | $24) { + break label$16 + } + __multf3($5 + 80 | 0, $18, $19, $8, $12, 0, 0, 0, 1073610752); + __addtf3($5 - -64 | 0, $10, $11, $15, $16, HEAP32[$5 + 80 >> 2], HEAP32[$5 + 84 >> 2], HEAP32[$5 + 88 >> 2], HEAP32[$5 + 92 >> 2]); + $15 = HEAP32[$5 + 72 >> 2]; + $16 = HEAP32[$5 + 76 >> 2]; + $24 = 1; + $10 = HEAP32[$5 + 64 >> 2]; + $11 = HEAP32[$5 + 68 >> 2]; + } + $9 = $9 + 1 | 0; + if ($9 >>> 0 < 1) { + $6 = $6 + 1 | 0 + } + $21 = 1; + } + $7 = HEAP32[$1 + 4 >> 2]; + if ($7 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $7 + 1; + $7 = HEAPU8[$7 | 0]; + } else { + $7 = __shgetc($1) + } + continue; + } + break; + }; + label$20 : { + label$21 : { + if (!$21) { + if (!HEAP32[$1 + 104 >> 2]) { + break label$21 + } + $2 = HEAP32[$1 + 4 >> 2]; + HEAP32[$1 + 4 >> 2] = $2 + -1; + HEAP32[$1 + 4 >> 2] = $2 + -2; + if (!$20) { + break label$21 + } + HEAP32[$1 + 4 >> 2] = $2 + -3; + break label$21; + } + if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 7 ? 0 : 1) : 0) { + $8 = $9; + $12 = $6; + while (1) { + $14 = $14 << 4; + $8 = $8 + 1 | 0; + if ($8 >>> 0 < 1) { + $12 = $12 + 1 | 0 + } + if (($8 | 0) != 8 | $12) { + continue + } + break; + }; + } + label$27 : { + if (($7 & -33) == 80) { + $8 = scanexp($1); + $7 = i64toi32_i32$HIGH_BITS; + $12 = $7; + if ($8 | ($7 | 0) != -2147483648) { + break label$27 + } + $8 = 0; + $12 = 0; + if (!HEAP32[$1 + 104 >> 2]) { + break label$27 + } + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; + break label$27; + } + $8 = 0; + $12 = 0; + if (!HEAP32[$1 + 104 >> 2]) { + break label$27 + } + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; + } + if (!$14) { + __extenddftf2($5 + 112 | 0, +($4 | 0) * 0.0); + $10 = HEAP32[$5 + 112 >> 2]; + $11 = HEAP32[$5 + 116 >> 2]; + $2 = HEAP32[$5 + 120 >> 2]; + $1 = HEAP32[$5 + 124 >> 2]; + break label$20; + } + $1 = $20 ? $17 : $9; + $6 = ($20 ? $13 : $6) << 2 | $1 >>> 30; + $1 = $8 + ($1 << 2) | 0; + $13 = $1 + -32 | 0; + $9 = $13; + $6 = $6 + $12 | 0; + $1 = ($1 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6) + -1 | 0; + $6 = $9 >>> 0 < 4294967264 ? $1 + 1 | 0 : $1; + if (($6 | 0) > 0 ? 1 : ($6 | 0) >= 0 ? ($9 >>> 0 <= 0 - $3 >>> 0 ? 0 : 1) : 0) { + HEAP32[2896] = 68; + __floatsitf($5 + 160 | 0, $4); + __multf3($5 + 144 | 0, HEAP32[$5 + 160 >> 2], HEAP32[$5 + 164 >> 2], HEAP32[$5 + 168 >> 2], HEAP32[$5 + 172 >> 2], -1, -1, -1, 2147418111); + __multf3($5 + 128 | 0, HEAP32[$5 + 144 >> 2], HEAP32[$5 + 148 >> 2], HEAP32[$5 + 152 >> 2], HEAP32[$5 + 156 >> 2], -1, -1, -1, 2147418111); + $10 = HEAP32[$5 + 128 >> 2]; + $11 = HEAP32[$5 + 132 >> 2]; + $2 = HEAP32[$5 + 136 >> 2]; + $1 = HEAP32[$5 + 140 >> 2]; + break label$20; + } + $1 = $3 + -226 | 0; + $7 = $9 >>> 0 < $1 >>> 0 ? 0 : 1; + $1 = $1 >> 31; + if (($6 | 0) > ($1 | 0) ? 1 : ($6 | 0) >= ($1 | 0) ? $7 : 0) { + if (($14 | 0) > -1) { + while (1) { + __addtf3($5 + 416 | 0, $10, $11, $15, $16, 0, 0, 0, -1073807360); + $1 = __getf2($10, $11, $15, $16, 1073610752); + $8 = ($1 | 0) < 0; + __addtf3($5 + 400 | 0, $10, $11, $15, $16, $8 ? $10 : HEAP32[$5 + 416 >> 2], $8 ? $11 : HEAP32[$5 + 420 >> 2], $8 ? $15 : HEAP32[$5 + 424 >> 2], $8 ? $16 : HEAP32[$5 + 428 >> 2]); + $6 = $6 + -1 | 0; + $9 = $9 + -1 | 0; + if (($9 | 0) != -1) { + $6 = $6 + 1 | 0 + } + $15 = HEAP32[$5 + 408 >> 2]; + $16 = HEAP32[$5 + 412 >> 2]; + $10 = HEAP32[$5 + 400 >> 2]; + $11 = HEAP32[$5 + 404 >> 2]; + $14 = $14 << 1 | ($1 | 0) > -1; + if (($14 | 0) > -1) { + continue + } + break; + } + } + $1 = ($9 - $3 | 0) + 32 | 0; + $8 = $1; + $7 = $2; + $12 = $1 >>> 0 >= $2 >>> 0 ? 0 : 1; + $2 = $6 - (($3 >> 31) + ($9 >>> 0 < $3 >>> 0) | 0) | 0; + $1 = $1 >>> 0 < 32 ? $2 + 1 | 0 : $2; + $1 = (($1 | 0) < 0 ? 1 : ($1 | 0) <= 0 ? $12 : 0) ? (($8 | 0) > 0 ? $8 : 0) : $7; + label$35 : { + if (($1 | 0) >= 113) { + __floatsitf($5 + 384 | 0, $4); + $17 = HEAP32[$5 + 392 >> 2]; + $13 = HEAP32[$5 + 396 >> 2]; + $18 = HEAP32[$5 + 384 >> 2]; + $19 = HEAP32[$5 + 388 >> 2]; + $6 = 0; + $4 = 0; + $3 = 0; + $2 = 0; + break label$35; + } + __extenddftf2($5 + 352 | 0, scalbn(1.0, 144 - $1 | 0)); + __floatsitf($5 + 336 | 0, $4); + $18 = HEAP32[$5 + 336 >> 2]; + $19 = HEAP32[$5 + 340 >> 2]; + $17 = HEAP32[$5 + 344 >> 2]; + $13 = HEAP32[$5 + 348 >> 2]; + copysignl($5 + 368 | 0, HEAP32[$5 + 352 >> 2], HEAP32[$5 + 356 >> 2], HEAP32[$5 + 360 >> 2], HEAP32[$5 + 364 >> 2], $18, $19, $17, $13); + $6 = HEAP32[$5 + 376 >> 2]; + $4 = HEAP32[$5 + 380 >> 2]; + $3 = HEAP32[$5 + 372 >> 2]; + $2 = HEAP32[$5 + 368 >> 2]; + } + $1 = !($14 & 1) & ((__letf2($10, $11, $15, $16, 0, 0, 0, 0) | 0) != 0 & ($1 | 0) < 32); + __floatunsitf($5 + 320 | 0, $1 + $14 | 0); + __multf3($5 + 304 | 0, $18, $19, $17, $13, HEAP32[$5 + 320 >> 2], HEAP32[$5 + 324 >> 2], HEAP32[$5 + 328 >> 2], HEAP32[$5 + 332 >> 2]); + __addtf3($5 + 272 | 0, HEAP32[$5 + 304 >> 2], HEAP32[$5 + 308 >> 2], HEAP32[$5 + 312 >> 2], HEAP32[$5 + 316 >> 2], $2, $3, $6, $4); + __multf3($5 + 288 | 0, $1 ? 0 : $10, $1 ? 0 : $11, $1 ? 0 : $15, $1 ? 0 : $16, $18, $19, $17, $13); + __addtf3($5 + 256 | 0, HEAP32[$5 + 288 >> 2], HEAP32[$5 + 292 >> 2], HEAP32[$5 + 296 >> 2], HEAP32[$5 + 300 >> 2], HEAP32[$5 + 272 >> 2], HEAP32[$5 + 276 >> 2], HEAP32[$5 + 280 >> 2], HEAP32[$5 + 284 >> 2]); + __subtf3($5 + 240 | 0, HEAP32[$5 + 256 >> 2], HEAP32[$5 + 260 >> 2], HEAP32[$5 + 264 >> 2], HEAP32[$5 + 268 >> 2], $2, $3, $6, $4); + $1 = HEAP32[$5 + 240 >> 2]; + $2 = HEAP32[$5 + 244 >> 2]; + $3 = HEAP32[$5 + 248 >> 2]; + $4 = HEAP32[$5 + 252 >> 2]; + if (!__letf2($1, $2, $3, $4, 0, 0, 0, 0)) { + HEAP32[2896] = 68 + } + scalbnl($5 + 224 | 0, $1, $2, $3, $4, $9); + $10 = HEAP32[$5 + 224 >> 2]; + $11 = HEAP32[$5 + 228 >> 2]; + $2 = HEAP32[$5 + 232 >> 2]; + $1 = HEAP32[$5 + 236 >> 2]; + break label$20; + } + HEAP32[2896] = 68; + __floatsitf($5 + 208 | 0, $4); + __multf3($5 + 192 | 0, HEAP32[$5 + 208 >> 2], HEAP32[$5 + 212 >> 2], HEAP32[$5 + 216 >> 2], HEAP32[$5 + 220 >> 2], 0, 0, 0, 65536); + __multf3($5 + 176 | 0, HEAP32[$5 + 192 >> 2], HEAP32[$5 + 196 >> 2], HEAP32[$5 + 200 >> 2], HEAP32[$5 + 204 >> 2], 0, 0, 0, 65536); + $10 = HEAP32[$5 + 176 >> 2]; + $11 = HEAP32[$5 + 180 >> 2]; + $2 = HEAP32[$5 + 184 >> 2]; + $1 = HEAP32[$5 + 188 >> 2]; + break label$20; + } + __extenddftf2($5 + 96 | 0, +($4 | 0) * 0.0); + $10 = HEAP32[$5 + 96 >> 2]; + $11 = HEAP32[$5 + 100 >> 2]; + $2 = HEAP32[$5 + 104 >> 2]; + $1 = HEAP32[$5 + 108 >> 2]; + } + HEAP32[$0 >> 2] = $10; + HEAP32[$0 + 4 >> 2] = $11; + HEAP32[$0 + 8 >> 2] = $2; + HEAP32[$0 + 12 >> 2] = $1; + global$0 = $5 + 432 | 0; + } + + function decfloat($0, $1, $2, $3, $4, $5) { + var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0.0, $25 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0, wasm2js_i32$2 = 0; + $6 = global$0 - 8976 | 0; + global$0 = $6; + $22 = $3 + $4 | 0; + $25 = 0 - $22 | 0; + label$1 : { + label$2 : { + while (1) { + if (($2 | 0) != 48) { + label$4 : { + if (($2 | 0) != 46) { + break label$1 + } + $2 = HEAP32[$1 + 4 >> 2]; + if ($2 >>> 0 >= HEAPU32[$1 + 104 >> 2]) { + break label$4 + } + HEAP32[$1 + 4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$2; + } + } else { + $2 = HEAP32[$1 + 4 >> 2]; + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + $9 = 1; + HEAP32[$1 + 4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + } else { + $9 = 1; + $2 = __shgetc($1); + } + continue; + } + break; + }; + $2 = __shgetc($1); + } + $14 = 1; + if (($2 | 0) != 48) { + break label$1 + } + while (1) { + $2 = HEAP32[$1 + 4 >> 2]; + label$8 : { + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$8; + } + $2 = __shgetc($1); + } + $7 = $7 + -1 | 0; + $8 = $8 + -1 | 0; + if (($8 | 0) != -1) { + $7 = $7 + 1 | 0 + } + if (($2 | 0) == 48) { + continue + } + break; + }; + $9 = 1; + } + HEAP32[$6 + 784 >> 2] = 0; + label$10 : { + label$11 : { + $12 = ($2 | 0) == 46; + $13 = $2 + -48 | 0; + label$13 : { + label$14 : { + label$15 : { + if ($12 | $13 >>> 0 <= 9) { + while (1) { + label$19 : { + if ($12 & 1) { + if (!$14) { + $8 = $10; + $7 = $11; + $14 = 1; + break label$19; + } + $9 = !$9; + break label$15; + } + $10 = $10 + 1 | 0; + if ($10 >>> 0 < 1) { + $11 = $11 + 1 | 0 + } + if (($15 | 0) <= 2044) { + $20 = ($2 | 0) == 48 ? $20 : $10; + $9 = ($6 + 784 | 0) + ($15 << 2) | 0; + HEAP32[$9 >> 2] = $17 ? (Math_imul(HEAP32[$9 >> 2], 10) + $2 | 0) + -48 | 0 : $13; + $9 = 1; + $13 = $17 + 1 | 0; + $2 = ($13 | 0) == 9; + $17 = $2 ? 0 : $13; + $15 = $2 + $15 | 0; + break label$19; + } + if (($2 | 0) == 48) { + break label$19 + } + HEAP32[$6 + 8960 >> 2] = HEAP32[$6 + 8960 >> 2] | 1; + $20 = 18396; + } + $2 = HEAP32[$1 + 4 >> 2]; + label$25 : { + if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { + HEAP32[$1 + 4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$25; + } + $2 = __shgetc($1); + } + $12 = ($2 | 0) == 46; + $13 = $2 + -48 | 0; + if ($12 | $13 >>> 0 < 10) { + continue + } + break; + } + } + $8 = $14 ? $8 : $10; + $7 = $14 ? $7 : $11; + if (!(!$9 | ($2 & -33) != 69)) { + $12 = scanexp($1); + $2 = i64toi32_i32$HIGH_BITS; + $16 = $2; + label$28 : { + if ($12 | ($2 | 0) != -2147483648) { + break label$28 + } + $12 = 0; + $16 = 0; + if (!HEAP32[$1 + 104 >> 2]) { + break label$28 + } + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; + } + if (!$9) { + break label$13 + } + $7 = $7 + $16 | 0; + $8 = $8 + $12 | 0; + if ($8 >>> 0 < $12 >>> 0) { + $7 = $7 + 1 | 0 + } + break label$11; + } + $9 = !$9; + if (($2 | 0) < 0) { + break label$14 + } + } + if (!HEAP32[$1 + 104 >> 2]) { + break label$14 + } + HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; + } + if (!$9) { + break label$11 + } + } + HEAP32[2896] = 28; + $10 = 0; + $11 = 0; + __shlim($1); + $2 = 0; + $1 = 0; + break label$10; + } + $1 = HEAP32[$6 + 784 >> 2]; + if (!$1) { + __extenddftf2($6, +($5 | 0) * 0.0); + $10 = HEAP32[$6 >> 2]; + $11 = HEAP32[$6 + 4 >> 2]; + $2 = HEAP32[$6 + 8 >> 2]; + $1 = HEAP32[$6 + 12 >> 2]; + break label$10; + } + if (!(($8 | 0) != ($10 | 0) | ($7 | 0) != ($11 | 0) | (($11 | 0) > 0 ? 1 : ($11 | 0) >= 0 ? ($10 >>> 0 <= 9 ? 0 : 1) : 0) | ($1 >>> $3 | 0 ? ($3 | 0) <= 30 : 0))) { + __floatsitf($6 + 48 | 0, $5); + __floatunsitf($6 + 32 | 0, $1); + __multf3($6 + 16 | 0, HEAP32[$6 + 48 >> 2], HEAP32[$6 + 52 >> 2], HEAP32[$6 + 56 >> 2], HEAP32[$6 + 60 >> 2], HEAP32[$6 + 32 >> 2], HEAP32[$6 + 36 >> 2], HEAP32[$6 + 40 >> 2], HEAP32[$6 + 44 >> 2]); + $10 = HEAP32[$6 + 16 >> 2]; + $11 = HEAP32[$6 + 20 >> 2]; + $2 = HEAP32[$6 + 24 >> 2]; + $1 = HEAP32[$6 + 28 >> 2]; + break label$10; + } + if (($7 | 0) > 0 ? 1 : ($7 | 0) >= 0 ? ($8 >>> 0 <= ($4 | 0) / -2 >>> 0 ? 0 : 1) : 0) { + HEAP32[2896] = 68; + __floatsitf($6 + 96 | 0, $5); + __multf3($6 + 80 | 0, HEAP32[$6 + 96 >> 2], HEAP32[$6 + 100 >> 2], HEAP32[$6 + 104 >> 2], HEAP32[$6 + 108 >> 2], -1, -1, -1, 2147418111); + __multf3($6 - -64 | 0, HEAP32[$6 + 80 >> 2], HEAP32[$6 + 84 >> 2], HEAP32[$6 + 88 >> 2], HEAP32[$6 + 92 >> 2], -1, -1, -1, 2147418111); + $10 = HEAP32[$6 + 64 >> 2]; + $11 = HEAP32[$6 + 68 >> 2]; + $2 = HEAP32[$6 + 72 >> 2]; + $1 = HEAP32[$6 + 76 >> 2]; + break label$10; + } + $1 = $4 + -226 | 0; + $2 = $8 >>> 0 >= $1 >>> 0 ? 0 : 1; + $1 = $1 >> 31; + if (($7 | 0) < ($1 | 0) ? 1 : ($7 | 0) <= ($1 | 0) ? $2 : 0) { + HEAP32[2896] = 68; + __floatsitf($6 + 144 | 0, $5); + __multf3($6 + 128 | 0, HEAP32[$6 + 144 >> 2], HEAP32[$6 + 148 >> 2], HEAP32[$6 + 152 >> 2], HEAP32[$6 + 156 >> 2], 0, 0, 0, 65536); + __multf3($6 + 112 | 0, HEAP32[$6 + 128 >> 2], HEAP32[$6 + 132 >> 2], HEAP32[$6 + 136 >> 2], HEAP32[$6 + 140 >> 2], 0, 0, 0, 65536); + $10 = HEAP32[$6 + 112 >> 2]; + $11 = HEAP32[$6 + 116 >> 2]; + $2 = HEAP32[$6 + 120 >> 2]; + $1 = HEAP32[$6 + 124 >> 2]; + break label$10; + } + if ($17) { + if (($17 | 0) <= 8) { + $2 = ($6 + 784 | 0) + ($15 << 2) | 0; + $1 = HEAP32[$2 >> 2]; + while (1) { + $1 = Math_imul($1, 10); + $17 = $17 + 1 | 0; + if (($17 | 0) != 9) { + continue + } + break; + }; + HEAP32[$2 >> 2] = $1; + } + $15 = $15 + 1 | 0; + } + label$36 : { + $14 = $8; + if (($20 | 0) > ($8 | 0) | ($20 | 0) >= 9 | ($8 | 0) > 17) { + break label$36 + } + if (($14 | 0) == 9) { + __floatsitf($6 + 192 | 0, $5); + __floatunsitf($6 + 176 | 0, HEAP32[$6 + 784 >> 2]); + __multf3($6 + 160 | 0, HEAP32[$6 + 192 >> 2], HEAP32[$6 + 196 >> 2], HEAP32[$6 + 200 >> 2], HEAP32[$6 + 204 >> 2], HEAP32[$6 + 176 >> 2], HEAP32[$6 + 180 >> 2], HEAP32[$6 + 184 >> 2], HEAP32[$6 + 188 >> 2]); + $10 = HEAP32[$6 + 160 >> 2]; + $11 = HEAP32[$6 + 164 >> 2]; + $2 = HEAP32[$6 + 168 >> 2]; + $1 = HEAP32[$6 + 172 >> 2]; + break label$10; + } + if (($14 | 0) <= 8) { + __floatsitf($6 + 272 | 0, $5); + __floatunsitf($6 + 256 | 0, HEAP32[$6 + 784 >> 2]); + __multf3($6 + 240 | 0, HEAP32[$6 + 272 >> 2], HEAP32[$6 + 276 >> 2], HEAP32[$6 + 280 >> 2], HEAP32[$6 + 284 >> 2], HEAP32[$6 + 256 >> 2], HEAP32[$6 + 260 >> 2], HEAP32[$6 + 264 >> 2], HEAP32[$6 + 268 >> 2]); + __floatsitf($6 + 224 | 0, HEAP32[(0 - $14 << 2) + 10560 >> 2]); + __divtf3($6 + 208 | 0, HEAP32[$6 + 240 >> 2], HEAP32[$6 + 244 >> 2], HEAP32[$6 + 248 >> 2], HEAP32[$6 + 252 >> 2], HEAP32[$6 + 224 >> 2], HEAP32[$6 + 228 >> 2], HEAP32[$6 + 232 >> 2], HEAP32[$6 + 236 >> 2]); + $10 = HEAP32[$6 + 208 >> 2]; + $11 = HEAP32[$6 + 212 >> 2]; + $2 = HEAP32[$6 + 216 >> 2]; + $1 = HEAP32[$6 + 220 >> 2]; + break label$10; + } + $1 = (Math_imul($14, -3) + $3 | 0) + 27 | 0; + $2 = HEAP32[$6 + 784 >> 2]; + if ($2 >>> $1 | 0 ? ($1 | 0) <= 30 : 0) { + break label$36 + } + __floatsitf($6 + 352 | 0, $5); + __floatunsitf($6 + 336 | 0, $2); + __multf3($6 + 320 | 0, HEAP32[$6 + 352 >> 2], HEAP32[$6 + 356 >> 2], HEAP32[$6 + 360 >> 2], HEAP32[$6 + 364 >> 2], HEAP32[$6 + 336 >> 2], HEAP32[$6 + 340 >> 2], HEAP32[$6 + 344 >> 2], HEAP32[$6 + 348 >> 2]); + __floatsitf($6 + 304 | 0, HEAP32[($14 << 2) + 10488 >> 2]); + __multf3($6 + 288 | 0, HEAP32[$6 + 320 >> 2], HEAP32[$6 + 324 >> 2], HEAP32[$6 + 328 >> 2], HEAP32[$6 + 332 >> 2], HEAP32[$6 + 304 >> 2], HEAP32[$6 + 308 >> 2], HEAP32[$6 + 312 >> 2], HEAP32[$6 + 316 >> 2]); + $10 = HEAP32[$6 + 288 >> 2]; + $11 = HEAP32[$6 + 292 >> 2]; + $2 = HEAP32[$6 + 296 >> 2]; + $1 = HEAP32[$6 + 300 >> 2]; + break label$10; + } + while (1) { + $2 = $15; + $15 = $2 + -1 | 0; + if (!HEAP32[($6 + 784 | 0) + ($15 << 2) >> 2]) { + continue + } + break; + }; + $17 = 0; + $1 = ($14 | 0) % 9 | 0; + label$40 : { + if (!$1) { + $9 = 0; + break label$40; + } + $13 = ($14 | 0) > -1 ? $1 : $1 + 9 | 0; + label$42 : { + if (!$2) { + $9 = 0; + $2 = 0; + break label$42; + } + $8 = HEAP32[(0 - $13 << 2) + 10560 >> 2]; + $10 = 1e9 / ($8 | 0) | 0; + $12 = 0; + $1 = 0; + $9 = 0; + while (1) { + $11 = ($6 + 784 | 0) + ($1 << 2) | 0; + $15 = HEAP32[$11 >> 2]; + $16 = ($15 >>> 0) / ($8 >>> 0) | 0; + $7 = $12 + $16 | 0; + HEAP32[$11 >> 2] = $7; + $7 = !$7 & ($1 | 0) == ($9 | 0); + $9 = $7 ? $9 + 1 & 2047 : $9; + $14 = $7 ? $14 + -9 | 0 : $14; + $12 = Math_imul($10, $15 - Math_imul($8, $16) | 0); + $1 = $1 + 1 | 0; + if (($2 | 0) != ($1 | 0)) { + continue + } + break; + }; + if (!$12) { + break label$42 + } + HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2] = $12; + $2 = $2 + 1 | 0; + } + $14 = ($14 - $13 | 0) + 9 | 0; + } + while (1) { + $11 = ($6 + 784 | 0) + ($9 << 2) | 0; + label$46 : { + while (1) { + if (($14 | 0) != 36 | HEAPU32[$11 >> 2] >= 10384593 ? ($14 | 0) >= 36 : 0) { + break label$46 + } + $15 = $2 + 2047 | 0; + $12 = 0; + $13 = $2; + while (1) { + $2 = $13; + $10 = $15 & 2047; + $13 = ($6 + 784 | 0) + ($10 << 2) | 0; + $1 = HEAP32[$13 >> 2]; + $7 = $1 >>> 3 | 0; + $1 = $1 << 29; + $8 = $1 + $12 | 0; + if ($8 >>> 0 < $1 >>> 0) { + $7 = $7 + 1 | 0 + } + $1 = 0; + if (!(!$7 & $8 >>> 0 < 1000000001 | $7 >>> 0 < 0)) { + $1 = __wasm_i64_udiv($8, $7, 1e9); + $8 = $8 - __wasm_i64_mul($1, i64toi32_i32$HIGH_BITS, 1e9, 0) | 0; + } + $12 = $1; + HEAP32[$13 >> 2] = $8; + $13 = ($10 | 0) != ($2 + -1 & 2047) ? $2 : ($9 | 0) == ($10 | 0) ? $2 : $8 ? $2 : $10; + $15 = $10 + -1 | 0; + if (($9 | 0) != ($10 | 0)) { + continue + } + break; + }; + $17 = $17 + -29 | 0; + if (!$12) { + continue + } + break; + }; + $9 = $9 + -1 & 2047; + if (($13 | 0) == ($9 | 0)) { + $1 = ($6 + 784 | 0) + (($13 + 2046 & 2047) << 2) | 0; + $2 = $13 + -1 & 2047; + HEAP32[$1 >> 2] = HEAP32[$1 >> 2] | HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2]; + } + $14 = $14 + 9 | 0; + HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2] = $12; + continue; + } + break; + }; + label$52 : { + label$53 : while (1) { + $8 = $2 + 1 & 2047; + $10 = ($6 + 784 | 0) + (($2 + -1 & 2047) << 2) | 0; + while (1) { + $7 = ($14 | 0) > 45 ? 9 : 1; + label$55 : { + while (1) { + $13 = $9; + $1 = 0; + label$57 : { + while (1) { + label$59 : { + $9 = $1 + $13 & 2047; + if (($9 | 0) == ($2 | 0)) { + break label$59 + } + $9 = HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2]; + $11 = HEAP32[($1 << 2) + 10512 >> 2]; + if ($9 >>> 0 < $11 >>> 0) { + break label$59 + } + if ($9 >>> 0 > $11 >>> 0) { + break label$57 + } + $1 = $1 + 1 | 0; + if (($1 | 0) != 4) { + continue + } + } + break; + }; + if (($14 | 0) != 36) { + break label$57 + } + $8 = 0; + $7 = 0; + $1 = 0; + $10 = 0; + $11 = 0; + while (1) { + $9 = $1 + $13 & 2047; + if (($9 | 0) == ($2 | 0)) { + $2 = $2 + 1 & 2047; + HEAP32[(($2 << 2) + $6 | 0) + 780 >> 2] = 0; + } + __multf3($6 + 768 | 0, $8, $7, $10, $11, 0, 0, 1342177280, 1075633366); + __floatunsitf($6 + 752 | 0, HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2]); + __addtf3($6 + 736 | 0, HEAP32[$6 + 768 >> 2], HEAP32[$6 + 772 >> 2], HEAP32[$6 + 776 >> 2], HEAP32[$6 + 780 >> 2], HEAP32[$6 + 752 >> 2], HEAP32[$6 + 756 >> 2], HEAP32[$6 + 760 >> 2], HEAP32[$6 + 764 >> 2]); + $10 = HEAP32[$6 + 744 >> 2]; + $11 = HEAP32[$6 + 748 >> 2]; + $8 = HEAP32[$6 + 736 >> 2]; + $7 = HEAP32[$6 + 740 >> 2]; + $1 = $1 + 1 | 0; + if (($1 | 0) != 4) { + continue + } + break; + }; + __floatsitf($6 + 720 | 0, $5); + __multf3($6 + 704 | 0, $8, $7, $10, $11, HEAP32[$6 + 720 >> 2], HEAP32[$6 + 724 >> 2], HEAP32[$6 + 728 >> 2], HEAP32[$6 + 732 >> 2]); + $10 = HEAP32[$6 + 712 >> 2]; + $11 = HEAP32[$6 + 716 >> 2]; + $8 = 0; + $7 = 0; + $12 = HEAP32[$6 + 704 >> 2]; + $16 = HEAP32[$6 + 708 >> 2]; + $23 = $17 + 113 | 0; + $4 = $23 - $4 | 0; + $20 = ($4 | 0) < ($3 | 0); + $1 = $20 ? (($4 | 0) > 0 ? $4 : 0) : $3; + if (($1 | 0) <= 112) { + break label$55 + } + $14 = 0; + $15 = 0; + $9 = 0; + $3 = 0; + break label$52; + } + $17 = $7 + $17 | 0; + $9 = $2; + if (($2 | 0) == ($13 | 0)) { + continue + } + break; + }; + $11 = 1e9 >>> $7 | 0; + $12 = -1 << $7 ^ -1; + $1 = 0; + $9 = $13; + while (1) { + $15 = ($6 + 784 | 0) + ($13 << 2) | 0; + $16 = HEAP32[$15 >> 2]; + $1 = $1 + ($16 >>> $7 | 0) | 0; + HEAP32[$15 >> 2] = $1; + $1 = !$1 & ($9 | 0) == ($13 | 0); + $9 = $1 ? $9 + 1 & 2047 : $9; + $14 = $1 ? $14 + -9 | 0 : $14; + $1 = Math_imul($11, $12 & $16); + $13 = $13 + 1 & 2047; + if (($13 | 0) != ($2 | 0)) { + continue + } + break; + }; + if (!$1) { + continue + } + if (($8 | 0) != ($9 | 0)) { + HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2] = $1; + $2 = $8; + continue label$53; + } + HEAP32[$10 >> 2] = HEAP32[$10 >> 2] | 1; + $9 = $8; + continue; + } + break; + }; + break; + }; + __extenddftf2($6 + 656 | 0, scalbn(1.0, 225 - $1 | 0)); + copysignl($6 + 688 | 0, HEAP32[$6 + 656 >> 2], HEAP32[$6 + 660 >> 2], HEAP32[$6 + 664 >> 2], HEAP32[$6 + 668 >> 2], $12, $16, $10, $11); + $9 = HEAP32[$6 + 696 >> 2]; + $3 = HEAP32[$6 + 700 >> 2]; + $14 = HEAP32[$6 + 688 >> 2]; + $15 = HEAP32[$6 + 692 >> 2]; + __extenddftf2($6 + 640 | 0, scalbn(1.0, 113 - $1 | 0)); + fmodl($6 + 672 | 0, $12, $16, $10, $11, HEAP32[$6 + 640 >> 2], HEAP32[$6 + 644 >> 2], HEAP32[$6 + 648 >> 2], HEAP32[$6 + 652 >> 2]); + $8 = HEAP32[$6 + 672 >> 2]; + $7 = HEAP32[$6 + 676 >> 2]; + $18 = HEAP32[$6 + 680 >> 2]; + $19 = HEAP32[$6 + 684 >> 2]; + __subtf3($6 + 624 | 0, $12, $16, $10, $11, $8, $7, $18, $19); + __addtf3($6 + 608 | 0, $14, $15, $9, $3, HEAP32[$6 + 624 >> 2], HEAP32[$6 + 628 >> 2], HEAP32[$6 + 632 >> 2], HEAP32[$6 + 636 >> 2]); + $10 = HEAP32[$6 + 616 >> 2]; + $11 = HEAP32[$6 + 620 >> 2]; + $12 = HEAP32[$6 + 608 >> 2]; + $16 = HEAP32[$6 + 612 >> 2]; + } + $21 = $13 + 4 & 2047; + label$64 : { + if (($21 | 0) == ($2 | 0)) { + break label$64 + } + $21 = HEAP32[($6 + 784 | 0) + ($21 << 2) >> 2]; + label$65 : { + if ($21 >>> 0 <= 499999999) { + if (($13 + 5 & 2047) == ($2 | 0) ? !$21 : 0) { + break label$65 + } + __extenddftf2($6 + 496 | 0, +($5 | 0) * .25); + __addtf3($6 + 480 | 0, $8, $7, $18, $19, HEAP32[$6 + 496 >> 2], HEAP32[$6 + 500 >> 2], HEAP32[$6 + 504 >> 2], HEAP32[$6 + 508 >> 2]); + $18 = HEAP32[$6 + 488 >> 2]; + $19 = HEAP32[$6 + 492 >> 2]; + $8 = HEAP32[$6 + 480 >> 2]; + $7 = HEAP32[$6 + 484 >> 2]; + break label$65; + } + if (($21 | 0) != 5e8) { + __extenddftf2($6 + 592 | 0, +($5 | 0) * .75); + __addtf3($6 + 576 | 0, $8, $7, $18, $19, HEAP32[$6 + 592 >> 2], HEAP32[$6 + 596 >> 2], HEAP32[$6 + 600 >> 2], HEAP32[$6 + 604 >> 2]); + $18 = HEAP32[$6 + 584 >> 2]; + $19 = HEAP32[$6 + 588 >> 2]; + $8 = HEAP32[$6 + 576 >> 2]; + $7 = HEAP32[$6 + 580 >> 2]; + break label$65; + } + $24 = +($5 | 0); + if (($13 + 5 & 2047) == ($2 | 0)) { + __extenddftf2($6 + 528 | 0, $24 * .5); + __addtf3($6 + 512 | 0, $8, $7, $18, $19, HEAP32[$6 + 528 >> 2], HEAP32[$6 + 532 >> 2], HEAP32[$6 + 536 >> 2], HEAP32[$6 + 540 >> 2]); + $18 = HEAP32[$6 + 520 >> 2]; + $19 = HEAP32[$6 + 524 >> 2]; + $8 = HEAP32[$6 + 512 >> 2]; + $7 = HEAP32[$6 + 516 >> 2]; + break label$65; + } + __extenddftf2($6 + 560 | 0, $24 * .75); + __addtf3($6 + 544 | 0, $8, $7, $18, $19, HEAP32[$6 + 560 >> 2], HEAP32[$6 + 564 >> 2], HEAP32[$6 + 568 >> 2], HEAP32[$6 + 572 >> 2]); + $18 = HEAP32[$6 + 552 >> 2]; + $19 = HEAP32[$6 + 556 >> 2]; + $8 = HEAP32[$6 + 544 >> 2]; + $7 = HEAP32[$6 + 548 >> 2]; + } + if (($1 | 0) > 111) { + break label$64 + } + fmodl($6 + 464 | 0, $8, $7, $18, $19, 0, 0, 0, 1073676288); + if (__letf2(HEAP32[$6 + 464 >> 2], HEAP32[$6 + 468 >> 2], HEAP32[$6 + 472 >> 2], HEAP32[$6 + 476 >> 2], 0, 0, 0, 0)) { + break label$64 + } + __addtf3($6 + 448 | 0, $8, $7, $18, $19, 0, 0, 0, 1073676288); + $18 = HEAP32[$6 + 456 >> 2]; + $19 = HEAP32[$6 + 460 >> 2]; + $8 = HEAP32[$6 + 448 >> 2]; + $7 = HEAP32[$6 + 452 >> 2]; + } + __addtf3($6 + 432 | 0, $12, $16, $10, $11, $8, $7, $18, $19); + __subtf3($6 + 416 | 0, HEAP32[$6 + 432 >> 2], HEAP32[$6 + 436 >> 2], HEAP32[$6 + 440 >> 2], HEAP32[$6 + 444 >> 2], $14, $15, $9, $3); + $10 = HEAP32[$6 + 424 >> 2]; + $11 = HEAP32[$6 + 428 >> 2]; + $12 = HEAP32[$6 + 416 >> 2]; + $16 = HEAP32[$6 + 420 >> 2]; + label$69 : { + if (($23 & 2147483647) <= (-2 - $22 | 0)) { + break label$69 + } + $2 = $6 + 400 | 0; + HEAP32[$2 + 8 >> 2] = $10; + HEAP32[$2 + 12 >> 2] = $11 & 2147483647; + HEAP32[$2 >> 2] = $12; + HEAP32[$2 + 4 >> 2] = $16; + __multf3($6 + 384 | 0, $12, $16, $10, $11, 0, 0, 0, 1073610752); + $3 = __getf2(HEAP32[$6 + 400 >> 2], HEAP32[$6 + 404 >> 2], HEAP32[$6 + 408 >> 2], HEAP32[$6 + 412 >> 2], 1081081856); + $2 = ($3 | 0) < 0; + $10 = $2 ? $10 : HEAP32[$6 + 392 >> 2]; + $11 = $2 ? $11 : HEAP32[$6 + 396 >> 2]; + $12 = $2 ? $12 : HEAP32[$6 + 384 >> 2]; + $16 = $2 ? $16 : HEAP32[$6 + 388 >> 2]; + $17 = (($3 | 0) > -1) + $17 | 0; + if (wasm2js_i32$0 = !($20 & ($2 | ($1 | 0) != ($4 | 0)) & (__letf2($8, $7, $18, $19, 0, 0, 0, 0) | 0) != 0), wasm2js_i32$1 = 0, wasm2js_i32$2 = ($17 + 110 | 0) <= ($25 | 0), wasm2js_i32$2 ? wasm2js_i32$0 : wasm2js_i32$1) { + break label$69 + } + HEAP32[2896] = 68; + } + scalbnl($6 + 368 | 0, $12, $16, $10, $11, $17); + $10 = HEAP32[$6 + 368 >> 2]; + $11 = HEAP32[$6 + 372 >> 2]; + $2 = HEAP32[$6 + 376 >> 2]; + $1 = HEAP32[$6 + 380 >> 2]; + } + HEAP32[$0 >> 2] = $10; + HEAP32[$0 + 4 >> 2] = $11; + HEAP32[$0 + 8 >> 2] = $2; + HEAP32[$0 + 12 >> 2] = $1; + global$0 = $6 + 8976 | 0; + } + + function scanexp($0) { + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0; + label$1 : { + label$2 : { + label$3 : { + $3 = HEAP32[$0 + 4 >> 2]; + label$4 : { + if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = $3 + 1; + $2 = HEAPU8[$3 | 0]; + break label$4; + } + $2 = __shgetc($0); + } + switch ($2 + -43 | 0) { + case 0: + case 2: + break label$2; + default: + break label$3; + }; + } + $1 = $2 + -48 | 0; + break label$1; + } + $5 = ($2 | 0) == 45; + $3 = HEAP32[$0 + 4 >> 2]; + label$6 : { + if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = $3 + 1; + $2 = HEAPU8[$3 | 0]; + break label$6; + } + $2 = __shgetc($0); + } + $1 = $2 + -48 | 0; + if (!($1 >>> 0 < 10 | !HEAP32[$0 + 104 >> 2])) { + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1 + } + } + label$9 : { + if ($1 >>> 0 < 10) { + $1 = 0; + while (1) { + $1 = Math_imul($1, 10) + $2 | 0; + $3 = HEAP32[$0 + 4 >> 2]; + label$12 : { + if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = $3 + 1; + $2 = HEAPU8[$3 | 0]; + break label$12; + } + $2 = __shgetc($0); + } + $4 = $2 + -48 | 0; + $1 = $1 + -48 | 0; + if (($1 | 0) < 214748364 ? $4 >>> 0 <= 9 : 0) { + continue + } + break; + }; + $3 = $1; + $1 = $1 >> 31; + label$14 : { + if ($4 >>> 0 >= 10) { + break label$14 + } + while (1) { + $1 = __wasm_i64_mul($3, $1, 10, 0); + $3 = $1 + $2 | 0; + $2 = i64toi32_i32$HIGH_BITS; + $4 = $3 >>> 0 < $1 >>> 0 ? $2 + 1 | 0 : $2; + $1 = HEAP32[$0 + 4 >> 2]; + label$16 : { + if ($1 >>> 0 < HEAPU32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = $1 + 1; + $2 = HEAPU8[$1 | 0]; + break label$16; + } + $2 = __shgetc($0); + } + $1 = $4 + -1 | 0; + $3 = $3 + -48 | 0; + if ($3 >>> 0 < 4294967248) { + $1 = $1 + 1 | 0 + } + $4 = $2 + -48 | 0; + if ($4 >>> 0 > 9) { + break label$14 + } + if (($1 | 0) < 21474836 ? 1 : ($1 | 0) <= 21474836 ? ($3 >>> 0 >= 2061584302 ? 0 : 1) : 0) { + continue + } + break; + }; + } + if ($4 >>> 0 < 10) { + while (1) { + $2 = HEAP32[$0 + 4 >> 2]; + label$20 : { + if ($2 >>> 0 < HEAPU32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = $2 + 1; + $2 = HEAPU8[$2 | 0]; + break label$20; + } + $2 = __shgetc($0); + } + if ($2 + -48 >>> 0 < 10) { + continue + } + break; + } + } + if (HEAP32[$0 + 104 >> 2]) { + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1 + } + $0 = $3; + $3 = $5 ? 0 - $0 | 0 : $0; + $1 = $5 ? 0 - ($1 + (0 < $0 >>> 0) | 0) | 0 : $1; + break label$9; + } + $3 = 0; + $1 = -2147483648; + if (!HEAP32[$0 + 104 >> 2]) { + break label$9 + } + HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1; + i64toi32_i32$HIGH_BITS = -2147483648; + return 0; + } + i64toi32_i32$HIGH_BITS = $1; + return $3; + } + + function strtox($0, $1) { + var $2 = 0, $3 = 0, $4 = 0; + $2 = global$0 - 160 | 0; + global$0 = $2; + memset($2 + 16 | 0, 144); + HEAP32[$2 + 92 >> 2] = -1; + HEAP32[$2 + 60 >> 2] = $1; + HEAP32[$2 + 24 >> 2] = -1; + HEAP32[$2 + 20 >> 2] = $1; + __shlim($2 + 16 | 0); + __floatscan($2, $2 + 16 | 0); + $1 = HEAP32[$2 + 8 >> 2]; + $3 = HEAP32[$2 + 12 >> 2]; + $4 = HEAP32[$2 + 4 >> 2]; + HEAP32[$0 >> 2] = HEAP32[$2 >> 2]; + HEAP32[$0 + 4 >> 2] = $4; + HEAP32[$0 + 8 >> 2] = $1; + HEAP32[$0 + 12 >> 2] = $3; + global$0 = $2 + 160 | 0; + } + + function strtod($0) { + var $1 = 0, $2 = 0.0; + $1 = global$0 - 16 | 0; + global$0 = $1; + strtox($1, $0); + $2 = __trunctfdf2(HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$1 + 8 >> 2], HEAP32[$1 + 12 >> 2]); + global$0 = $1 + 16 | 0; + return $2; + } + + function FLAC__stream_encoder_new() { + var $0 = 0, $1 = 0, $2 = 0, $3 = 0; + $1 = dlcalloc(1, 8); + if (!$1) { + return 0 + } + $0 = dlcalloc(1, 1032); + HEAP32[$1 >> 2] = $0; + label$2 : { + if (!$0) { + break label$2 + } + $3 = dlcalloc(1, 11856); + HEAP32[$1 + 4 >> 2] = $3; + if (!$3) { + dlfree($0); + break label$2; + } + $0 = dlcalloc(1, 20); + $3 = HEAP32[$1 + 4 >> 2]; + HEAP32[$3 + 6856 >> 2] = $0; + if (!$0) { + dlfree($3); + dlfree(HEAP32[$1 >> 2]); + break label$2; + } + HEAP32[$3 + 7296 >> 2] = 0; + $0 = HEAP32[$1 >> 2]; + HEAP32[$0 + 44 >> 2] = 13; + HEAP32[$0 + 48 >> 2] = 1056964608; + HEAP32[$0 + 36 >> 2] = 0; + HEAP32[$0 + 40 >> 2] = 1; + HEAP32[$0 + 28 >> 2] = 16; + HEAP32[$0 + 32 >> 2] = 44100; + HEAP32[$0 + 20 >> 2] = 0; + HEAP32[$0 + 24 >> 2] = 2; + HEAP32[$0 + 12 >> 2] = 1; + HEAP32[$0 + 16 >> 2] = 0; + HEAP32[$0 + 4 >> 2] = 0; + HEAP32[$0 + 8 >> 2] = 1; + $0 = HEAP32[$1 >> 2]; + HEAP32[$0 + 592 >> 2] = 0; + HEAP32[$0 + 596 >> 2] = 0; + HEAP32[$0 + 556 >> 2] = 0; + HEAP32[$0 + 560 >> 2] = 0; + HEAP32[$0 + 564 >> 2] = 0; + HEAP32[$0 + 568 >> 2] = 0; + HEAP32[$0 + 572 >> 2] = 0; + HEAP32[$0 + 576 >> 2] = 0; + HEAP32[$0 + 580 >> 2] = 0; + HEAP32[$0 + 584 >> 2] = 0; + HEAP32[$0 + 600 >> 2] = 0; + HEAP32[$0 + 604 >> 2] = 0; + $3 = HEAP32[$1 + 4 >> 2]; + $2 = $3; + HEAP32[$2 + 7248 >> 2] = 0; + HEAP32[$2 + 7252 >> 2] = 0; + HEAP32[$2 + 7048 >> 2] = 0; + $2 = $2 + 7256 | 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + $2 = $3 + 7264 | 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + $2 = $3 + 7272 | 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + $2 = $3 + 7280 | 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + HEAP32[$3 + 7288 >> 2] = 0; + FLAC__ogg_encoder_aspect_set_defaults($0 + 632 | 0); + $0 = HEAP32[$1 >> 2]; + label$5 : { + if (HEAP32[$0 >> 2] != 1) { + break label$5 + } + HEAP32[$0 + 16 >> 2] = 1; + HEAP32[$0 + 20 >> 2] = 0; + FLAC__stream_encoder_set_apodization($1, 10777); + $0 = HEAP32[$1 >> 2]; + if (HEAP32[$0 >> 2] != 1) { + break label$5 + } + HEAP32[$0 + 576 >> 2] = 0; + HEAP32[$0 + 580 >> 2] = 5; + HEAP32[$0 + 564 >> 2] = 0; + HEAP32[$0 + 568 >> 2] = 0; + HEAP32[$0 + 556 >> 2] = 8; + HEAP32[$0 + 560 >> 2] = 0; + } + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 11848 >> 2] = 0; + HEAP32[$0 + 6176 >> 2] = $0 + 336; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6180 >> 2] = $0 + 628; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6184 >> 2] = $0 + 920; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6188 >> 2] = $0 + 1212; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6192 >> 2] = $0 + 1504; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6196 >> 2] = $0 + 1796; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6200 >> 2] = $0 + 2088; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6204 >> 2] = $0 + 2380; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6208 >> 2] = $0 + 2672; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6212 >> 2] = $0 + 2964; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6216 >> 2] = $0 + 3256; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6220 >> 2] = $0 + 3548; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6224 >> 2] = $0 + 3840; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6228 >> 2] = $0 + 4132; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6232 >> 2] = $0 + 4424; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6236 >> 2] = $0 + 4716; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6240 >> 2] = $0 + 5008; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6244 >> 2] = $0 + 5300; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6248 >> 2] = $0 + 5592; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6252 >> 2] = $0 + 5884; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6640 >> 2] = $0 + 6256; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6644 >> 2] = $0 + 6268; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6648 >> 2] = $0 + 6280; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6652 >> 2] = $0 + 6292; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6656 >> 2] = $0 + 6304; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6660 >> 2] = $0 + 6316; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6664 >> 2] = $0 + 6328; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6668 >> 2] = $0 + 6340; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6672 >> 2] = $0 + 6352; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6676 >> 2] = $0 + 6364; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6680 >> 2] = $0 + 6376; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6684 >> 2] = $0 + 6388; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6688 >> 2] = $0 + 6400; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6692 >> 2] = $0 + 6412; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6696 >> 2] = $0 + 6424; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6700 >> 2] = $0 + 6436; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6704 >> 2] = $0 + 6448; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6708 >> 2] = $0 + 6460; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6712 >> 2] = $0 + 6472; + $0 = HEAP32[$1 + 4 >> 2]; + HEAP32[$0 + 6716 >> 2] = $0 + 6484; + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6256 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6268 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6280 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6292 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6304 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6316 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6328 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6340 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6352 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6364 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6376 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6388 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6400 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6412 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6424 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6436 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6448 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6460 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6472 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6484 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 11724 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 11736 | 0); + HEAP32[HEAP32[$1 >> 2] >> 2] = 1; + return $1 | 0; + } + dlfree($1); + return 0; + } + + function FLAC__stream_encoder_set_apodization($0, $1) { + var $2 = 0, $3 = 0, $4 = 0, $5 = Math_fround(0), $6 = Math_fround(0), $7 = 0, $8 = 0.0, $9 = Math_fround(0), $10 = 0, $11 = 0; + $2 = HEAP32[$0 >> 2]; + label$1 : { + if (HEAP32[$2 >> 2] != 1) { + break label$1 + } + HEAP32[$2 + 40 >> 2] = 0; + while (1) { + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + label$8 : { + label$9 : { + label$10 : { + label$11 : { + label$12 : { + label$13 : { + label$14 : { + label$15 : { + label$16 : { + $10 = strchr($1, 59); + label$17 : { + if ($10) { + $4 = $10 - $1 | 0; + break label$17; + } + $4 = strlen($1); + } + $11 = ($4 | 0) != 8; + if (!$11) { + if (strncmp(10584, $1, 8)) { + break label$16 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 0; + break label$3; + } + label$20 : { + switch ($4 + -6 | 0) { + case 1: + break label$13; + case 0: + break label$14; + case 20: + break label$15; + case 7: + break label$20; + default: + break label$12; + }; + } + $7 = 1; + if (strncmp(10593, $1, 13)) { + break label$11 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 1; + break label$3; + } + $7 = 0; + if (strncmp(10607, $1, 8)) { + break label$11 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 2; + break label$3; + } + $7 = 0; + if (strncmp(10616, $1, 26)) { + break label$11 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 3; + break label$3; + } + if (strncmp(10643, $1, 6)) { + break label$3 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 4; + break label$3; + } + if (strncmp(10650, $1, 7)) { + break label$10 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 5; + break label$3; + } + $7 = 0; + if ($4 >>> 0 < 8) { + break label$9 + } + } + if (strncmp(10658, $1, 6)) { + break label$8 + } + $6 = Math_fround(strtod($1 + 6 | 0)); + if ($6 > Math_fround(0.0) ^ 1 | $6 <= Math_fround(.5) ^ 1) { + break label$3 + } + $1 = HEAP32[$0 >> 2]; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + $4 = HEAP32[$1 + 40 >> 2]; + HEAP32[$1 + 40 >> 2] = $4 + 1; + HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 6; + break label$3; + } + if (strncmp(10665, $1, 7)) { + break label$7 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 7; + break label$3; + } + label$21 : { + switch ($4 + -4 | 0) { + case 0: + break label$21; + case 1: + break label$5; + default: + break label$3; + }; + } + if (strncmp(10673, $1, 4)) { + break label$3 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 8; + break label$3; + } + if (!$7) { + break label$6 + } + if (strncmp(10678, $1, 13)) { + break label$6 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 9; + break label$3; + } + if (strncmp(10692, $1, 7)) { + break label$3 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 10; + break label$3; + } + label$22 : { + if (($4 | 0) != 9) { + break label$22 + } + if (strncmp(10700, $1, 9)) { + break label$22 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 11; + break label$3; + } + if (!$11) { + if (!strncmp(10710, $1, 8)) { + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 12; + break label$3; + } + if (strncmp(10719, $1, 6)) { + break label$3 + } + break label$4; + } + if (!strncmp(10719, $1, 6)) { + break label$4 + } + if ($4 >>> 0 < 16) { + break label$3 + } + if (!strncmp(10726, $1, 14)) { + $8 = strtod($1 + 14 | 0); + label$26 : { + if (Math_abs($8) < 2147483648.0) { + $4 = ~~$8; + break label$26; + } + $4 = -2147483648; + } + $3 = strchr($1, 47); + $5 = Math_fround(.10000000149011612); + label$28 : { + if (!$3) { + break label$28 + } + $2 = $3 + 1 | 0; + $5 = Math_fround(.9900000095367432); + if (!(Math_fround(strtod($2)) < Math_fround(.9900000095367432))) { + break label$28 + } + $5 = Math_fround(strtod($2)); + } + $1 = strchr($3 ? $3 + 1 | 0 : $1, 47); + $6 = Math_fround(.20000000298023224); + label$30 : { + if (!$1) { + break label$30 + } + $6 = Math_fround(strtod($1 + 1 | 0)); + } + $1 = HEAP32[$0 >> 2]; + $2 = HEAP32[$1 + 40 >> 2]; + if (($4 | 0) <= 1) { + HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + $4 = HEAP32[$1 + 40 >> 2]; + HEAP32[$1 + 40 >> 2] = $4 + 1; + HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; + break label$3; + } + if ($2 + $4 >>> 0 > 31) { + break label$3 + } + $9 = Math_fround(Math_fround(Math_fround(1.0) / Math_fround(Math_fround(1.0) - $5)) + Math_fround(-1.0)); + $5 = Math_fround($9 + Math_fround($4 | 0)); + $3 = 0; + while (1) { + HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 52 >> 2] = Math_fround($3 | 0) / $5; + $1 = HEAP32[$0 >> 2]; + $3 = $3 + 1 | 0; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 56 >> 2] = Math_fround($9 + Math_fround($3 | 0)) / $5; + $1 = HEAP32[$0 >> 2]; + $7 = HEAP32[$1 + 40 >> 2]; + $2 = $7 + 1 | 0; + HEAP32[$1 + 40 >> 2] = $2; + HEAP32[(($7 << 4) + $1 | 0) + 44 >> 2] = 14; + if (($3 | 0) != ($4 | 0)) { + continue + } + break; + }; + break label$3; + } + if ($4 >>> 0 < 17) { + break label$3 + } + if (strncmp(10741, $1, 15)) { + break label$3 + } + $8 = strtod($1 + 15 | 0); + label$33 : { + if (Math_abs($8) < 2147483648.0) { + $4 = ~~$8; + break label$33; + } + $4 = -2147483648; + } + $6 = Math_fround(.20000000298023224); + $3 = strchr($1, 47); + $5 = Math_fround(.20000000298023224); + label$35 : { + if (!$3) { + break label$35 + } + $2 = $3 + 1 | 0; + $5 = Math_fround(.9900000095367432); + if (!(Math_fround(strtod($2)) < Math_fround(.9900000095367432))) { + break label$35 + } + $5 = Math_fround(strtod($2)); + } + $1 = strchr($3 ? $3 + 1 | 0 : $1, 47); + if ($1) { + $6 = Math_fround(strtod($1 + 1 | 0)) + } + $1 = HEAP32[$0 >> 2]; + $2 = HEAP32[$1 + 40 >> 2]; + if (($4 | 0) <= 1) { + HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + $4 = HEAP32[$1 + 40 >> 2]; + HEAP32[$1 + 40 >> 2] = $4 + 1; + HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; + break label$3; + } + if ($2 + $4 >>> 0 > 31) { + break label$3 + } + $9 = Math_fround(Math_fround(Math_fround(1.0) / Math_fround(Math_fround(1.0) - $5)) + Math_fround(-1.0)); + $5 = Math_fround($9 + Math_fround($4 | 0)); + $3 = 0; + while (1) { + HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 52 >> 2] = Math_fround($3 | 0) / $5; + $1 = HEAP32[$0 >> 2]; + $3 = $3 + 1 | 0; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 56 >> 2] = Math_fround($9 + Math_fround($3 | 0)) / $5; + $1 = HEAP32[$0 >> 2]; + $7 = HEAP32[$1 + 40 >> 2]; + $2 = $7 + 1 | 0; + HEAP32[$1 + 40 >> 2] = $2; + HEAP32[(($7 << 4) + $1 | 0) + 44 >> 2] = 15; + if (($3 | 0) != ($4 | 0)) { + continue + } + break; + }; + break label$3; + } + if (strncmp(10757, $1, 5)) { + break label$3 + } + HEAP32[$2 + 40 >> 2] = $3 + 1; + HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 16; + break label$3; + } + $6 = Math_fround(strtod($1 + 6 | 0)); + if ($6 >= Math_fround(0.0) ^ 1 | $6 <= Math_fround(1.0) ^ 1) { + break label$3 + } + $1 = HEAP32[$0 >> 2]; + HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 48 >> 2] = $6; + $1 = HEAP32[$0 >> 2]; + $4 = HEAP32[$1 + 40 >> 2]; + HEAP32[$1 + 40 >> 2] = $4 + 1; + HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; + } + $2 = HEAP32[$0 >> 2]; + $3 = HEAP32[$2 + 40 >> 2]; + if ($10) { + $1 = $10 + 1 | 0; + if (($3 | 0) != 32) { + continue + } + } + break; + }; + $4 = 1; + if ($3) { + break label$1 + } + HEAP32[$2 + 40 >> 2] = 1; + HEAP32[$2 + 44 >> 2] = 13; + HEAP32[$2 + 48 >> 2] = 1056964608; + } + return $4; + } + + function FLAC__stream_encoder_delete($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0; + if ($0) { + HEAP32[HEAP32[$0 + 4 >> 2] + 11848 >> 2] = 1; + FLAC__stream_encoder_finish($0); + $1 = HEAP32[$0 + 4 >> 2]; + $2 = HEAP32[$1 + 11752 >> 2]; + if ($2) { + FLAC__stream_decoder_delete($2); + $1 = HEAP32[$0 + 4 >> 2]; + } + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear($1 + 6256 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6268 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6280 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6292 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6304 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6316 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6328 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6340 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6352 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6364 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6376 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6388 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6400 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6412 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6424 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6436 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6448 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6460 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6472 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6484 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 11724 | 0); + FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 11736 | 0); + FLAC__bitreader_delete(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); + dlfree(HEAP32[$0 + 4 >> 2]); + dlfree(HEAP32[$0 >> 2]); + dlfree($0); + } + } + + function FLAC__stream_encoder_finish($0) { + $0 = $0 | 0; + var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; + $7 = global$0 - 32 | 0; + global$0 = $7; + label$1 : { + if (!$0) { + break label$1 + } + label$3 : { + label$4 : { + $5 = HEAP32[$0 >> 2]; + $1 = HEAP32[$5 >> 2]; + switch ($1 | 0) { + case 1: + break label$1; + case 0: + break label$4; + default: + break label$3; + }; + } + $2 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$2 + 11848 >> 2]) { + break label$3 + } + $2 = HEAP32[$2 + 7052 >> 2]; + if (!$2) { + break label$3 + } + $3 = HEAP32[$5 + 36 >> 2]; + HEAP32[$5 + 36 >> 2] = $2; + $3 = !process_frame_($0, ($2 | 0) != ($3 | 0), 1); + $5 = HEAP32[$0 >> 2]; + } + if (HEAP32[$5 + 12 >> 2]) { + $2 = HEAP32[$0 + 4 >> 2]; + FLAC__MD5Final($2 + 6928 | 0, $2 + 7060 | 0); + } + $5 = $0 + 4 | 0; + $1 = HEAP32[$0 + 4 >> 2]; + label$6 : { + if (HEAP32[$1 + 11848 >> 2]) { + $2 = $3; + break label$6; + } + $4 = HEAP32[$0 >> 2]; + label$8 : { + if (HEAP32[$4 >> 2]) { + break label$8 + } + $11 = HEAP32[$1 + 7268 >> 2]; + if ($11) { + label$10 : { + if (HEAP32[$1 + 7260 >> 2]) { + $13 = HEAP32[$1 + 6900 >> 2]; + $12 = HEAP32[$1 + 6896 >> 2]; + $2 = $1 + 6920 | 0; + $8 = HEAP32[$2 >> 2]; + $9 = HEAP32[$2 + 4 >> 2]; + if ((FUNCTION_TABLE[$11]($0, 0, 0, HEAP32[$1 + 7288 >> 2]) | 0) == 2) { + break label$10 + } + simple_ogg_page__init($7); + $2 = HEAP32[$0 >> 2]; + $4 = HEAP32[$2 + 608 >> 2]; + $6 = HEAP32[$2 + 612 >> 2]; + $2 = HEAP32[$0 + 4 >> 2]; + label$12 : { + if (!simple_ogg_page__get_at($0, $4, $6, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7264 >> 2], HEAP32[$2 + 7288 >> 2])) { + break label$12 + } + $11 = HEAP32[1357] + HEAP32[1356] | 0; + $14 = HEAP32[1362] + (HEAP32[1361] + (HEAP32[1360] + (HEAP32[1359] + ($11 + HEAP32[1358] | 0) | 0) | 0) | 0) | 0; + $2 = $14 + HEAP32[1363] >>> 3 | 0; + if ($2 + 33 >>> 0 > HEAPU32[$7 + 12 >> 2]) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + simple_ogg_page__clear($7); + break label$10; + } + $4 = $1 + 6936 | 0; + $10 = HEAPU8[$4 + 4 | 0] | HEAPU8[$4 + 5 | 0] << 8 | (HEAPU8[$4 + 6 | 0] << 16 | HEAPU8[$4 + 7 | 0] << 24); + $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; + $4 = HEAPU8[$4 | 0] | HEAPU8[$4 + 1 | 0] << 8 | (HEAPU8[$4 + 2 | 0] << 16 | HEAPU8[$4 + 3 | 0] << 24); + HEAP8[$2 + 25 | 0] = $4; + HEAP8[$2 + 26 | 0] = $4 >>> 8; + HEAP8[$2 + 27 | 0] = $4 >>> 16; + HEAP8[$2 + 28 | 0] = $4 >>> 24; + HEAP8[$2 + 29 | 0] = $10; + HEAP8[$2 + 30 | 0] = $10 >>> 8; + HEAP8[$2 + 31 | 0] = $10 >>> 16; + HEAP8[$2 + 32 | 0] = $10 >>> 24; + $1 = $1 + 6928 | 0; + $4 = HEAPU8[$1 + 4 | 0] | HEAPU8[$1 + 5 | 0] << 8 | (HEAPU8[$1 + 6 | 0] << 16 | HEAPU8[$1 + 7 | 0] << 24); + $1 = HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24); + HEAP8[$2 + 17 | 0] = $1; + HEAP8[$2 + 18 | 0] = $1 >>> 8; + HEAP8[$2 + 19 | 0] = $1 >>> 16; + HEAP8[$2 + 20 | 0] = $1 >>> 24; + HEAP8[$2 + 21 | 0] = $4; + HEAP8[$2 + 22 | 0] = $4 >>> 8; + HEAP8[$2 + 23 | 0] = $4 >>> 16; + HEAP8[$2 + 24 | 0] = $4 >>> 24; + $2 = $14 + -4 >>> 3 | 0; + if ($2 + 22 >>> 0 > HEAPU32[$7 + 12 >> 2]) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + simple_ogg_page__clear($7); + break label$10; + } + $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; + HEAP8[$2 + 21 | 0] = $8; + HEAP8[$2 + 20 | 0] = ($9 & 255) << 24 | $8 >>> 8; + HEAP8[$2 + 19 | 0] = ($9 & 65535) << 16 | $8 >>> 16; + HEAP8[$2 + 18 | 0] = ($9 & 16777215) << 8 | $8 >>> 24; + $2 = $2 + 17 | 0; + HEAP8[$2 | 0] = HEAPU8[$2 | 0] & 240 | $9 & 15; + $2 = $11 >>> 3 | 0; + if ($2 + 23 >>> 0 > HEAPU32[$7 + 12 >> 2]) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + simple_ogg_page__clear($7); + break label$10; + } + $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; + HEAP8[$2 + 22 | 0] = $13; + HEAP8[$2 + 21 | 0] = $13 >>> 8; + HEAP8[$2 + 20 | 0] = $13 >>> 16; + HEAP8[$2 + 19 | 0] = $12; + HEAP8[$2 + 18 | 0] = $12 >>> 8; + HEAP8[$2 + 17 | 0] = $12 >>> 16; + $2 = HEAP32[$0 >> 2]; + $4 = HEAP32[$2 + 608 >> 2]; + $1 = HEAP32[$2 + 612 >> 2]; + $2 = HEAP32[$0 + 4 >> 2]; + $2 = simple_ogg_page__set_at($0, $4, $1, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7276 >> 2], HEAP32[$2 + 7288 >> 2]); + simple_ogg_page__clear($7); + if (!$2) { + break label$10 + } + $2 = HEAP32[HEAP32[$5 >> 2] + 7048 >> 2]; + if (!$2 | !HEAP32[$2 >> 2]) { + break label$10 + } + $1 = HEAP32[$0 >> 2]; + if (!(HEAP32[$1 + 616 >> 2] | HEAP32[$1 + 620 >> 2])) { + break label$10 + } + FLAC__format_seektable_sort($2); + simple_ogg_page__init($7); + $2 = HEAP32[$0 >> 2]; + $4 = HEAP32[$2 + 616 >> 2]; + $1 = HEAP32[$2 + 620 >> 2]; + $2 = HEAP32[$0 + 4 >> 2]; + if (!simple_ogg_page__get_at($0, $4, $1, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7264 >> 2], HEAP32[$2 + 7288 >> 2])) { + break label$12 + } + $6 = HEAP32[$5 >> 2]; + $2 = HEAP32[$6 + 7048 >> 2]; + $1 = HEAP32[$2 >> 2]; + if (HEAP32[$7 + 12 >> 2] != (Math_imul($1, 18) + 4 | 0)) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + simple_ogg_page__clear($7); + break label$10; + } + if ($1) { + $1 = HEAP32[$7 + 8 >> 2] + 4 | 0; + $4 = 0; + while (1) { + $8 = HEAP32[$2 + 4 >> 2] + Math_imul($4, 24) | 0; + $9 = HEAP32[$8 >> 2]; + $2 = HEAP32[$8 + 4 >> 2]; + $10 = HEAP32[$8 + 8 >> 2]; + $6 = HEAP32[$8 + 12 >> 2]; + $8 = HEAP32[$8 + 16 >> 2]; + HEAP8[$1 + 17 | 0] = $8; + HEAP8[$1 + 15 | 0] = $10; + HEAP8[$1 + 7 | 0] = $9; + HEAP8[$1 + 16 | 0] = $8 >>> 8; + HEAP8[$1 + 14 | 0] = ($6 & 255) << 24 | $10 >>> 8; + HEAP8[$1 + 13 | 0] = ($6 & 65535) << 16 | $10 >>> 16; + HEAP8[$1 + 12 | 0] = ($6 & 16777215) << 8 | $10 >>> 24; + HEAP8[$1 + 11 | 0] = $6; + HEAP8[$1 + 10 | 0] = $6 >>> 8; + HEAP8[$1 + 9 | 0] = $6 >>> 16; + HEAP8[$1 + 8 | 0] = $6 >>> 24; + HEAP8[$1 + 6 | 0] = ($2 & 255) << 24 | $9 >>> 8; + HEAP8[$1 + 5 | 0] = ($2 & 65535) << 16 | $9 >>> 16; + HEAP8[$1 + 4 | 0] = ($2 & 16777215) << 8 | $9 >>> 24; + HEAP8[$1 + 3 | 0] = $2; + HEAP8[$1 + 2 | 0] = $2 >>> 8; + HEAP8[$1 + 1 | 0] = $2 >>> 16; + HEAP8[$1 | 0] = $2 >>> 24; + $1 = $1 + 18 | 0; + $4 = $4 + 1 | 0; + $6 = HEAP32[$5 >> 2]; + $2 = HEAP32[$6 + 7048 >> 2]; + if ($4 >>> 0 < HEAPU32[$2 >> 2]) { + continue + } + break; + }; + } + $2 = HEAP32[$0 >> 2]; + simple_ogg_page__set_at($0, HEAP32[$2 + 616 >> 2], HEAP32[$2 + 620 >> 2], $7, HEAP32[$6 + 7268 >> 2], HEAP32[$6 + 7276 >> 2], HEAP32[$6 + 7288 >> 2]); + } + simple_ogg_page__clear($7); + break label$10; + } + $13 = HEAP32[$1 + 6912 >> 2]; + $8 = HEAP32[$1 + 6900 >> 2]; + $9 = HEAP32[$1 + 6896 >> 2]; + $6 = $1 + 6920 | 0; + $2 = HEAP32[$6 >> 2]; + $6 = HEAP32[$6 + 4 >> 2]; + label$19 : { + label$20 : { + $16 = $0; + $10 = HEAP32[$4 + 612 >> 2]; + $12 = HEAP32[1357] + HEAP32[1356] | 0; + $14 = HEAP32[1362] + (HEAP32[1361] + (HEAP32[1360] + (HEAP32[1359] + ($12 + HEAP32[1358] | 0) | 0) | 0) | 0) | 0; + $15 = ($14 + HEAP32[1363] >>> 3 | 0) + 4 | 0; + $4 = $15 + HEAP32[$4 + 608 >> 2] | 0; + if ($4 >>> 0 < $15 >>> 0) { + $10 = $10 + 1 | 0 + } + switch (FUNCTION_TABLE[$11]($16, $4, $10, HEAP32[$1 + 7288 >> 2]) | 0) { + case 0: + break label$19; + case 1: + break label$20; + default: + break label$10; + }; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + $4 = $1 + 6928 | 0; + $1 = HEAP32[$0 + 4 >> 2]; + if (FUNCTION_TABLE[HEAP32[$1 + 7276 >> 2]]($0, $4, 16, 0, 0, HEAP32[$1 + 7288 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + HEAP8[$7 + 4 | 0] = $2; + HEAP8[$7 + 3 | 0] = ($6 & 255) << 24 | $2 >>> 8; + HEAP8[$7 + 2 | 0] = ($6 & 65535) << 16 | $2 >>> 16; + HEAP8[$7 + 1 | 0] = ($6 & 16777215) << 8 | $2 >>> 24; + HEAP8[$7 | 0] = ($6 & 15 | $13 << 4) + 240; + label$22 : { + label$23 : { + $2 = ($14 + -4 >>> 3 | 0) + 4 | 0; + $1 = HEAP32[$0 >> 2]; + $4 = $2 + HEAP32[$1 + 608 >> 2] | 0; + $1 = HEAP32[$1 + 612 >> 2]; + $1 = $4 >>> 0 < $2 >>> 0 ? $1 + 1 | 0 : $1; + $2 = HEAP32[$0 + 4 >> 2]; + switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $4, $1, HEAP32[$2 + 7288 >> 2]) | 0) { + case 0: + break label$22; + case 1: + break label$23; + default: + break label$10; + }; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + $2 = HEAP32[$0 + 4 >> 2]; + if (FUNCTION_TABLE[HEAP32[$2 + 7276 >> 2]]($0, $7, 5, 0, 0, HEAP32[$2 + 7288 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + HEAP8[$7 + 5 | 0] = $8; + HEAP8[$7 + 4 | 0] = $8 >>> 8; + HEAP8[$7 + 3 | 0] = $8 >>> 16; + HEAP8[$7 + 2 | 0] = $9; + HEAP8[$7 + 1 | 0] = $9 >>> 8; + HEAP8[$7 | 0] = $9 >>> 16; + label$25 : { + label$26 : { + $2 = ($12 >>> 3 | 0) + 4 | 0; + $1 = HEAP32[$0 >> 2]; + $4 = $2 + HEAP32[$1 + 608 >> 2] | 0; + $1 = HEAP32[$1 + 612 >> 2]; + $1 = $4 >>> 0 < $2 >>> 0 ? $1 + 1 | 0 : $1; + $2 = HEAP32[$0 + 4 >> 2]; + switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $4, $1, HEAP32[$2 + 7288 >> 2]) | 0) { + case 0: + break label$25; + case 1: + break label$26; + default: + break label$10; + }; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + $2 = HEAP32[$0 + 4 >> 2]; + if (FUNCTION_TABLE[HEAP32[$2 + 7276 >> 2]]($0, $7, 6, 0, 0, HEAP32[$2 + 7288 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + $2 = HEAP32[HEAP32[$5 >> 2] + 7048 >> 2]; + if (!$2 | !HEAP32[$2 >> 2]) { + break label$10 + } + $1 = HEAP32[$0 >> 2]; + if (!(HEAP32[$1 + 616 >> 2] | HEAP32[$1 + 620 >> 2])) { + break label$10 + } + FLAC__format_seektable_sort($2); + label$28 : { + label$29 : { + label$30 : { + $2 = HEAP32[$0 >> 2]; + $1 = HEAP32[$2 + 616 >> 2] + 4 | 0; + $2 = HEAP32[$2 + 620 >> 2]; + $4 = $1 >>> 0 < 4 ? $2 + 1 | 0 : $2; + $2 = HEAP32[$0 + 4 >> 2]; + switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $1, $4, HEAP32[$2 + 7288 >> 2]) | 0) { + case 1: + break label$29; + case 0: + break label$30; + default: + break label$10; + }; + } + $4 = HEAP32[$5 >> 2]; + $1 = HEAP32[$4 + 7048 >> 2]; + if (!HEAP32[$1 >> 2]) { + break label$10 + } + $6 = 0; + break label$28; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + break label$10; + } + while (1) { + label$32 : { + $9 = Math_imul($6, 24); + $8 = $9 + HEAP32[$1 + 4 >> 2] | 0; + $2 = HEAP32[$8 + 4 >> 2]; + $8 = HEAP32[$8 >> 2]; + $10 = $8 << 24 | $8 << 8 & 16711680; + HEAP32[$7 >> 2] = (($2 & 255) << 24 | $8 >>> 8) & -16777216 | (($2 & 16777215) << 8 | $8 >>> 24) & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); + HEAP32[$7 + 4 >> 2] = ($2 << 24 | $8 >>> 8) & 65280 | ($2 << 8 | $8 >>> 24) & 255 | $10; + $8 = $9 + HEAP32[$1 + 4 >> 2] | 0; + $2 = HEAP32[$8 + 12 >> 2]; + $8 = HEAP32[$8 + 8 >> 2]; + $10 = $8 << 24 | $8 << 8 & 16711680; + HEAP32[$7 + 8 >> 2] = (($2 & 255) << 24 | $8 >>> 8) & -16777216 | (($2 & 16777215) << 8 | $8 >>> 24) & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); + HEAP32[$7 + 12 >> 2] = ($2 << 24 | $8 >>> 8) & 65280 | ($2 << 8 | $8 >>> 24) & 255 | $10; + $2 = HEAPU16[($9 + HEAP32[$1 + 4 >> 2] | 0) + 16 >> 1]; + HEAP16[$7 + 16 >> 1] = ($2 << 24 | $2 << 8 & 16711680) >>> 16; + if (FUNCTION_TABLE[HEAP32[$4 + 7276 >> 2]]($0, $7, 18, 0, 0, HEAP32[$4 + 7288 >> 2])) { + break label$32 + } + $6 = $6 + 1 | 0; + $4 = HEAP32[$5 >> 2]; + $1 = HEAP32[$4 + 7048 >> 2]; + if ($6 >>> 0 < HEAPU32[$1 >> 2]) { + continue + } + break label$10; + } + break; + }; + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + } + $1 = HEAP32[$0 + 4 >> 2]; + $4 = HEAP32[$0 >> 2]; + $3 = HEAP32[$4 >> 2] ? 1 : $3; + } + $2 = HEAP32[$1 + 7280 >> 2]; + if (!$2) { + break label$8 + } + FUNCTION_TABLE[$2]($0, $1 + 6872 | 0, HEAP32[$1 + 7288 >> 2]); + $4 = HEAP32[$0 >> 2]; + } + if (!HEAP32[$4 + 4 >> 2]) { + $2 = $3; + break label$6; + } + $2 = HEAP32[HEAP32[$5 >> 2] + 11752 >> 2]; + if (!$2) { + $2 = $3; + break label$6; + } + if (FLAC__stream_decoder_finish($2)) { + $2 = $3; + break label$6; + } + $2 = 1; + if ($3) { + break label$6 + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 4; + } + $1 = HEAP32[$5 >> 2]; + $3 = HEAP32[$1 + 7296 >> 2]; + if ($3) { + if (($3 | 0) != HEAP32[1896]) { + fclose($3); + $1 = HEAP32[$5 >> 2]; + } + HEAP32[$1 + 7296 >> 2] = 0; + } + if (HEAP32[$1 + 7260 >> 2]) { + ogg_stream_clear(HEAP32[$0 >> 2] + 640 | 0) + } + $1 = HEAP32[$0 >> 2]; + $3 = HEAP32[$1 + 600 >> 2]; + if ($3) { + dlfree($3); + $1 = HEAP32[$0 >> 2]; + HEAP32[$1 + 600 >> 2] = 0; + HEAP32[$1 + 604 >> 2] = 0; + } + if (HEAP32[$1 + 24 >> 2]) { + $3 = 0; + while (1) { + $4 = HEAP32[$5 >> 2]; + $1 = $3 << 2; + $6 = HEAP32[($4 + $1 | 0) + 7328 >> 2]; + if ($6) { + dlfree($6); + HEAP32[($1 + HEAP32[$5 >> 2] | 0) + 7328 >> 2] = 0; + $4 = HEAP32[$5 >> 2]; + } + $4 = HEAP32[($4 + $1 | 0) + 7368 >> 2]; + if ($4) { + dlfree($4); + HEAP32[($1 + HEAP32[$5 >> 2] | 0) + 7368 >> 2] = 0; + } + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + }; + } + $1 = HEAP32[$5 >> 2]; + $3 = HEAP32[$1 + 7360 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7360 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7400 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7400 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7364 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7364 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7404 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7404 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $4 = HEAP32[$0 >> 2]; + if (HEAP32[$4 + 40 >> 2]) { + $3 = 0; + while (1) { + $6 = $3 << 2; + $8 = HEAP32[($6 + $1 | 0) + 7408 >> 2]; + if ($8) { + dlfree($8); + HEAP32[($6 + HEAP32[$0 + 4 >> 2] | 0) + 7408 >> 2] = 0; + $4 = HEAP32[$0 >> 2]; + $1 = HEAP32[$0 + 4 >> 2]; + } + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[$4 + 40 >> 2]) { + continue + } + break; + }; + } + $3 = HEAP32[$1 + 7536 >> 2]; + if ($3) { + dlfree($3); + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 7536 >> 2] = 0; + $4 = HEAP32[$0 >> 2]; + } + if (HEAP32[$4 + 24 >> 2]) { + $4 = 0; + while (1) { + $3 = $4 << 3; + $6 = HEAP32[($3 + $1 | 0) + 7540 >> 2]; + if ($6) { + dlfree($6); + HEAP32[($3 + HEAP32[$5 >> 2] | 0) + 7540 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $6 = HEAP32[($1 + $3 | 0) + 7544 >> 2]; + if ($6) { + dlfree($6); + HEAP32[($3 + HEAP32[$5 >> 2] | 0) + 7544 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $4 = $4 + 1 | 0; + if ($4 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + }; + } + $3 = HEAP32[$1 + 7604 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7604 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7608 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7608 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7612 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7612 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7616 >> 2]; + if ($3) { + dlfree($3); + HEAP32[HEAP32[$5 >> 2] + 7616 >> 2] = 0; + $1 = HEAP32[$5 >> 2]; + } + $3 = HEAP32[$1 + 7620 >> 2]; + if ($3) { + dlfree($3); + $1 = HEAP32[$5 >> 2]; + HEAP32[$1 + 7620 >> 2] = 0; + } + $3 = HEAP32[$1 + 7624 >> 2]; + if ($3) { + dlfree($3); + $1 = HEAP32[$5 >> 2]; + HEAP32[$1 + 7624 >> 2] = 0; + } + $3 = HEAP32[$0 >> 2]; + if (!(!HEAP32[$3 + 4 >> 2] | !HEAP32[$3 + 24 >> 2])) { + $5 = 0; + while (1) { + $4 = $5 << 2; + $6 = HEAP32[($4 + $1 | 0) + 11764 >> 2]; + if ($6) { + dlfree($6); + HEAP32[($4 + HEAP32[$0 + 4 >> 2] | 0) + 11764 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + $3 = HEAP32[$0 >> 2]; + } + $5 = $5 + 1 | 0; + if ($5 >>> 0 < HEAPU32[$3 + 24 >> 2]) { + continue + } + break; + }; + } + FLAC__bitwriter_free(HEAP32[$1 + 6856 >> 2]); + $3 = HEAP32[$0 >> 2]; + HEAP32[$3 + 44 >> 2] = 13; + HEAP32[$3 + 48 >> 2] = 1056964608; + HEAP32[$3 + 36 >> 2] = 0; + HEAP32[$3 + 40 >> 2] = 1; + HEAP32[$3 + 28 >> 2] = 16; + HEAP32[$3 + 32 >> 2] = 44100; + HEAP32[$3 + 20 >> 2] = 0; + HEAP32[$3 + 24 >> 2] = 2; + HEAP32[$3 + 12 >> 2] = 1; + HEAP32[$3 + 16 >> 2] = 0; + HEAP32[$3 + 4 >> 2] = 0; + HEAP32[$3 + 8 >> 2] = 1; + $3 = HEAP32[$0 >> 2]; + HEAP32[$3 + 592 >> 2] = 0; + HEAP32[$3 + 596 >> 2] = 0; + HEAP32[$3 + 556 >> 2] = 0; + HEAP32[$3 + 560 >> 2] = 0; + HEAP32[$3 + 564 >> 2] = 0; + HEAP32[$3 + 568 >> 2] = 0; + HEAP32[$3 + 572 >> 2] = 0; + HEAP32[$3 + 576 >> 2] = 0; + HEAP32[$3 + 580 >> 2] = 0; + HEAP32[$3 + 584 >> 2] = 0; + HEAP32[$3 + 600 >> 2] = 0; + HEAP32[$3 + 604 >> 2] = 0; + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 7248 >> 2] = 0; + HEAP32[$1 + 7252 >> 2] = 0; + HEAP32[$1 + 7048 >> 2] = 0; + $5 = $1 + 7256 | 0; + HEAP32[$5 >> 2] = 0; + HEAP32[$5 + 4 >> 2] = 0; + $5 = $1 + 7264 | 0; + HEAP32[$5 >> 2] = 0; + HEAP32[$5 + 4 >> 2] = 0; + $5 = $1 + 7272 | 0; + HEAP32[$5 >> 2] = 0; + HEAP32[$5 + 4 >> 2] = 0; + $5 = $1 + 7280 | 0; + HEAP32[$5 >> 2] = 0; + HEAP32[$5 + 4 >> 2] = 0; + HEAP32[$1 + 7288 >> 2] = 0; + FLAC__ogg_encoder_aspect_set_defaults($3 + 632 | 0); + $1 = HEAP32[$0 >> 2]; + label$74 : { + if (HEAP32[$1 >> 2] != 1) { + break label$74 + } + HEAP32[$1 + 16 >> 2] = 1; + HEAP32[$1 + 20 >> 2] = 0; + FLAC__stream_encoder_set_apodization($0, 10777); + $1 = HEAP32[$0 >> 2]; + if (HEAP32[$1 >> 2] != 1) { + break label$74 + } + HEAP32[$1 + 576 >> 2] = 0; + HEAP32[$1 + 580 >> 2] = 5; + HEAP32[$1 + 564 >> 2] = 0; + HEAP32[$1 + 568 >> 2] = 0; + HEAP32[$1 + 556 >> 2] = 8; + HEAP32[$1 + 560 >> 2] = 0; + } + if (!$2) { + HEAP32[$1 >> 2] = 1 + } + $1 = !$2; + } + global$0 = $7 + 32 | 0; + return $1 | 0; + } + + function process_frame_($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; + $8 = global$0 - 48 | 0; + global$0 = $8; + label$1 : { + label$2 : { + $4 = HEAP32[$0 >> 2]; + if (!HEAP32[$4 + 12 >> 2]) { + break label$2 + } + $3 = HEAP32[$0 + 4 >> 2]; + $3 = FLAC__MD5Accumulate($3 + 7060 | 0, $3 + 4 | 0, HEAP32[$4 + 24 >> 2], HEAP32[$4 + 36 >> 2], HEAP32[$4 + 28 >> 2] + 7 >>> 3 | 0); + $4 = HEAP32[$0 >> 2]; + if ($3) { + break label$2 + } + HEAP32[$4 >> 2] = 8; + $1 = 0; + break label$1; + } + $3 = HEAP32[$4 + 576 >> 2]; + if ($1) { + $12 = 0 + } else { + $1 = FLAC__format_get_max_rice_partition_order_from_blocksize(HEAP32[$4 + 36 >> 2]); + $4 = HEAP32[$0 >> 2]; + $5 = HEAP32[$4 + 580 >> 2]; + $12 = $1 >>> 0 < $5 >>> 0 ? $1 : $5; + } + $7 = HEAP32[$4 + 36 >> 2]; + HEAP32[$8 + 8 >> 2] = $7; + HEAP32[$8 + 12 >> 2] = HEAP32[$4 + 32 >> 2]; + $1 = HEAP32[$4 + 24 >> 2]; + HEAP32[$8 + 20 >> 2] = 0; + HEAP32[$8 + 16 >> 2] = $1; + $1 = HEAP32[$4 + 28 >> 2]; + HEAP32[$8 + 28 >> 2] = 0; + HEAP32[$8 + 24 >> 2] = $1; + $5 = HEAP32[$0 + 4 >> 2]; + HEAP32[$8 + 32 >> 2] = HEAP32[$5 + 7056 >> 2]; + $14 = $3 >>> 0 < $12 >>> 0 ? $3 : $12; + label$5 : { + label$6 : { + label$7 : { + label$8 : { + label$9 : { + label$10 : { + label$11 : { + if (!HEAP32[$4 + 16 >> 2]) { + $10 = 1; + break label$11; + } + if (!HEAP32[$4 + 20 >> 2] | !HEAP32[$5 + 6864 >> 2]) { + break label$11 + } + $10 = 1; + $13 = 1; + if (HEAP32[$5 + 6868 >> 2]) { + break label$10 + } + } + label$13 : { + if (!HEAP32[$4 + 24 >> 2]) { + $3 = 0; + break label$13; + } + while (1) { + $13 = ($6 << 2) + $5 | 0; + $3 = 0; + $11 = 0; + label$16 : { + if (!$7) { + break label$16 + } + $15 = HEAP32[$13 + 4 >> 2]; + $1 = 0; + while (1) { + label$18 : { + $3 = HEAP32[$15 + ($1 << 2) >> 2] | $3; + $9 = $3 & 1; + $1 = $1 + 1 | 0; + if ($1 >>> 0 >= $7 >>> 0) { + break label$18 + } + if (!$9) { + continue + } + } + break; + }; + $1 = 0; + $11 = 0; + if (!$3) { + break label$16 + } + $11 = 0; + if ($9) { + break label$16 + } + while (1) { + $1 = $1 + 1 | 0; + $9 = $3 & 2; + $3 = $3 >> 1; + if (!$9) { + continue + } + break; + }; + $9 = 0; + $11 = 0; + if (!$1) { + break label$16 + } + while (1) { + $3 = $15 + ($9 << 2) | 0; + HEAP32[$3 >> 2] = HEAP32[$3 >> 2] >> $1; + $9 = $9 + 1 | 0; + if (($9 | 0) != ($7 | 0)) { + continue + } + break; + }; + $11 = $1; + } + $1 = $11; + $7 = Math_imul($6, 584) + $5 | 0; + $3 = HEAP32[$4 + 28 >> 2]; + $1 = $1 >>> 0 > $3 >>> 0 ? $3 : $1; + HEAP32[$7 + 624 >> 2] = $1; + HEAP32[$7 + 916 >> 2] = $1; + HEAP32[$13 + 216 >> 2] = $3 - $1; + $6 = $6 + 1 | 0; + $3 = HEAP32[$4 + 24 >> 2]; + if ($6 >>> 0 >= $3 >>> 0) { + break label$13 + } + $7 = HEAP32[$4 + 36 >> 2]; + continue; + }; + } + $1 = 1; + if ($10) { + break label$9 + } + $7 = HEAP32[$4 + 36 >> 2]; + $13 = 0; + } + $9 = HEAP32[$5 + 36 >> 2]; + $3 = 0; + $6 = 0; + label$21 : { + if (!$7) { + break label$21 + } + $1 = 0; + while (1) { + label$23 : { + $1 = HEAP32[($6 << 2) + $9 >> 2] | $1; + $10 = $1 & 1; + $6 = $6 + 1 | 0; + if ($6 >>> 0 >= $7 >>> 0) { + break label$23 + } + if (!$10) { + continue + } + } + break; + }; + $6 = 0; + if ($10 | !$1) { + break label$21 + } + while (1) { + $6 = $6 + 1 | 0; + $10 = $1 & 2; + $1 = $1 >> 1; + if (!$10) { + continue + } + break; + }; + $1 = 0; + if (!$6) { + $6 = 0; + break label$21; + } + while (1) { + $10 = ($1 << 2) + $9 | 0; + HEAP32[$10 >> 2] = HEAP32[$10 >> 2] >> $6; + $1 = $1 + 1 | 0; + if (($7 | 0) != ($1 | 0)) { + continue + } + break; + }; + } + $1 = HEAP32[$4 + 28 >> 2]; + $6 = $6 >>> 0 > $1 >>> 0 ? $1 : $6; + HEAP32[$5 + 5296 >> 2] = $6; + HEAP32[$5 + 5588 >> 2] = $6; + HEAP32[$5 + 248 >> 2] = $1 - $6; + $6 = HEAP32[$4 + 36 >> 2]; + label$27 : { + if (!$6) { + break label$27 + } + $7 = HEAP32[$5 + 40 >> 2]; + $1 = 0; + while (1) { + label$29 : { + $3 = HEAP32[$7 + ($1 << 2) >> 2] | $3; + $10 = $3 & 1; + $1 = $1 + 1 | 0; + if ($1 >>> 0 >= $6 >>> 0) { + break label$29 + } + if (!$10) { + continue + } + } + break; + }; + $1 = 0; + if (!$3) { + $3 = 0; + break label$27; + } + if ($10) { + $3 = 0; + break label$27; + } + while (1) { + $1 = $1 + 1 | 0; + $10 = $3 & 2; + $3 = $3 >> 1; + if (!$10) { + continue + } + break; + }; + $3 = 0; + if (!$1) { + break label$27 + } + while (1) { + $10 = $7 + ($3 << 2) | 0; + HEAP32[$10 >> 2] = HEAP32[$10 >> 2] >> $1; + $3 = $3 + 1 | 0; + if (($6 | 0) != ($3 | 0)) { + continue + } + break; + }; + $3 = $1; + } + $1 = HEAP32[$4 + 28 >> 2]; + $3 = $3 >>> 0 > $1 >>> 0 ? $1 : $3; + HEAP32[$5 + 5880 >> 2] = $3; + HEAP32[$5 + 6172 >> 2] = $3; + HEAP32[$5 + 252 >> 2] = ($1 - $3 | 0) + 1; + if ($13) { + break label$8 + } + $3 = HEAP32[$4 + 24 >> 2]; + $1 = 0; + } + $4 = $1; + if ($3) { + $3 = 0; + while (1) { + $1 = ($3 << 2) + $5 | 0; + $5 = ($3 << 3) + $5 | 0; + process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$1 + 216 >> 2], HEAP32[$1 + 4 >> 2], $5 + 6176 | 0, $5 + 6640 | 0, $5 + 256 | 0, $1 + 6768 | 0, $1 + 6808 | 0); + $5 = HEAP32[$0 + 4 >> 2]; + $3 = $3 + 1 | 0; + if ($3 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + }; + } + if ($4) { + break label$7 + } + $9 = HEAP32[$5 + 36 >> 2]; + } + process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$5 + 248 >> 2], $9, $5 + 6240 | 0, $5 + 6704 | 0, $5 + 320 | 0, $5 + 6800 | 0, $5 + 6840 | 0); + $1 = HEAP32[$0 + 4 >> 2]; + process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$1 + 252 >> 2], HEAP32[$1 + 40 >> 2], $1 + 6248 | 0, $1 + 6712 | 0, $1 + 328 | 0, $1 + 6804 | 0, $1 + 6844 | 0); + $11 = $8; + $1 = HEAP32[$0 + 4 >> 2]; + label$36 : { + if (!(!HEAP32[HEAP32[$0 >> 2] + 20 >> 2] | !HEAP32[$1 + 6864 >> 2])) { + $3 = HEAP32[$1 + 6868 >> 2] ? 3 : 0; + break label$36; + } + $3 = HEAP32[$1 + 6844 >> 2]; + $5 = HEAP32[$1 + 6808 >> 2]; + $4 = $3 + $5 | 0; + $6 = HEAP32[$1 + 6812 >> 2]; + $5 = $5 + $6 | 0; + $7 = $4 >>> 0 < $5 >>> 0; + $6 = $3 + $6 | 0; + $5 = $7 ? $4 : $5; + $4 = $6 >>> 0 < $5 >>> 0; + $3 = $3 + HEAP32[$1 + 6840 >> 2] >>> 0 < ($4 ? $6 : $5) >>> 0 ? 3 : $4 ? 2 : $7; + } + HEAP32[$11 + 20 >> 2] = $3; + if (!FLAC__frame_add_header($8 + 8 | 0, HEAP32[$1 + 6856 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + $1 = 0; + break label$1; + } + $5 = $0; + $6 = HEAP32[$8 + 8 >> 2]; + label$39 : { + label$40 : { + switch ($3 | 0) { + default: + $3 = HEAP32[$0 + 4 >> 2]; + $7 = 0; + $1 = 0; + $4 = 0; + $9 = 0; + break label$39; + case 0: + $3 = HEAP32[$0 + 4 >> 2]; + $4 = $3 + 336 | 0; + $1 = $4 + Math_imul(HEAP32[$3 + 6768 >> 2], 292) | 0; + $7 = ($4 + Math_imul(HEAP32[$3 + 6772 >> 2], 292) | 0) + 584 | 0; + $4 = HEAP32[$3 + 216 >> 2]; + $9 = HEAP32[$3 + 220 >> 2]; + break label$39; + case 1: + $3 = HEAP32[$0 + 4 >> 2]; + $1 = ($3 + Math_imul(HEAP32[$3 + 6768 >> 2], 292) | 0) + 336 | 0; + $7 = (Math_imul(HEAP32[$3 + 6804 >> 2], 292) + $3 | 0) + 5592 | 0; + $4 = HEAP32[$3 + 216 >> 2]; + $9 = HEAP32[$3 + 252 >> 2]; + break label$39; + case 2: + $3 = HEAP32[$0 + 4 >> 2]; + $7 = ($3 + Math_imul(HEAP32[$3 + 6772 >> 2], 292) | 0) + 920 | 0; + $1 = (Math_imul(HEAP32[$3 + 6804 >> 2], 292) + $3 | 0) + 5592 | 0; + $4 = HEAP32[$3 + 252 >> 2]; + $9 = HEAP32[$3 + 220 >> 2]; + break label$39; + case 3: + break label$40; + }; + } + $3 = HEAP32[$0 + 4 >> 2]; + $4 = $3 + 5008 | 0; + $1 = $4 + Math_imul(HEAP32[$3 + 6800 >> 2], 292) | 0; + $7 = ($4 + Math_imul(HEAP32[$3 + 6804 >> 2], 292) | 0) + 584 | 0; + $4 = HEAP32[$3 + 248 >> 2]; + $9 = HEAP32[$3 + 252 >> 2]; + } + if (!add_subframe_($5, $6, $4, $1, HEAP32[$3 + 6856 >> 2])) { + break label$6 + } + if (!add_subframe_($0, HEAP32[$8 + 8 >> 2], $9, $7, HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2])) { + break label$6 + } + $1 = HEAP32[$0 >> 2]; + break label$5; + } + $3 = FLAC__frame_add_header($8 + 8 | 0, HEAP32[$5 + 6856 >> 2]); + $1 = HEAP32[$0 >> 2]; + if ($3) { + if (!HEAP32[$1 + 24 >> 2]) { + break label$5 + } + $3 = 0; + while (1) { + $1 = HEAP32[$0 + 4 >> 2]; + $5 = $1 + ($3 << 2) | 0; + if (!add_subframe_($0, HEAP32[$8 + 8 >> 2], HEAP32[$5 + 216 >> 2], (($1 + Math_imul($3, 584) | 0) + Math_imul(HEAP32[$5 + 6768 >> 2], 292) | 0) + 336 | 0, HEAP32[$1 + 6856 >> 2])) { + break label$6 + } + $3 = $3 + 1 | 0; + $1 = HEAP32[$0 >> 2]; + if ($3 >>> 0 < HEAPU32[$1 + 24 >> 2]) { + continue + } + break; + }; + break label$5; + } + HEAP32[$1 >> 2] = 7; + } + $1 = 0; + break label$1; + } + if (HEAP32[$1 + 20 >> 2]) { + $1 = HEAP32[$0 + 4 >> 2]; + $3 = HEAP32[$1 + 6864 >> 2] + 1 | 0; + HEAP32[$1 + 6864 >> 2] = $3 >>> 0 < HEAPU32[$1 + 6860 >> 2] ? $3 : 0; + } + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 6868 >> 2] = HEAP32[$8 + 20 >> 2]; + $1 = HEAP32[$1 + 6856 >> 2]; + $3 = HEAP32[$1 + 16 >> 2] & 7; + $11 = 1; + __inlined_func$FLAC__bitwriter_zero_pad_to_byte_boundary : { + if (!$3) { + break __inlined_func$FLAC__bitwriter_zero_pad_to_byte_boundary + } + $11 = FLAC__bitwriter_write_zeroes($1, 8 - $3 | 0); + } + if (!$11) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $1 = 0; + break label$1; + } + label$49 : { + if (FLAC__bitwriter_get_write_crc16(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], $8 + 8 | 0)) { + if (FLAC__bitwriter_write_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], HEAPU16[$8 + 8 >> 1], HEAP32[1404])) { + break label$49 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 8; + $1 = 0; + break label$1; + } + $1 = 0; + if (!write_bitbuffer_($0, HEAP32[HEAP32[$0 >> 2] + 36 >> 2], $2)) { + break label$1 + } + $1 = HEAP32[$0 + 4 >> 2]; + HEAP32[$1 + 7052 >> 2] = 0; + HEAP32[$1 + 7056 >> 2] = HEAP32[$1 + 7056 >> 2] + 1; + $2 = $1 + 6920 | 0; + $3 = $2; + $11 = $3; + $1 = HEAP32[$3 + 4 >> 2]; + $0 = HEAP32[HEAP32[$0 >> 2] + 36 >> 2]; + $2 = $0 + HEAP32[$3 >> 2] | 0; + if ($2 >>> 0 < $0 >>> 0) { + $1 = $1 + 1 | 0 + } + HEAP32[$11 >> 2] = $2; + HEAP32[$3 + 4 >> 2] = $1; + $1 = 1; + } + $0 = $1; + global$0 = $8 + 48 | 0; + return $0; + } + + function FLAC__stream_encoder_init_stream($0, $1, $2, $3, $4, $5) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + return init_stream_internal__1($0, 0, $1, $2, $3, $4, $5, 0) | 0; + } + + function init_stream_internal__1($0, $1, $2, $3, $4, $5, $6, $7) { + var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0.0, $17 = 0, $18 = 0, $19 = 0; + $15 = global$0 - 176 | 0; + global$0 = $15; + $9 = 13; + $8 = HEAP32[$0 >> 2]; + label$1 : { + if (HEAP32[$8 >> 2] != 1) { + break label$1 + } + $9 = 3; + if (!$2 | ($4 ? 0 : $3)) { + break label$1 + } + $9 = 4; + $11 = HEAP32[$8 + 24 >> 2]; + if ($11 + -1 >>> 0 > 7) { + break label$1 + } + label$2 : { + label$3 : { + if (($11 | 0) != 2) { + HEAP32[$8 + 16 >> 2] = 0; + break label$3; + } + if (HEAP32[$8 + 16 >> 2]) { + break label$2 + } + } + HEAP32[$8 + 20 >> 2] = 0; + } + $11 = HEAP32[$8 + 28 >> 2]; + if ($11 >>> 0 >= 32) { + HEAP32[$8 + 16 >> 2] = 0; + $9 = 5; + break label$1; + } + $9 = 5; + if ($11 + -4 >>> 0 > 20) { + break label$1 + } + if (HEAP32[$8 + 32 >> 2] + -1 >>> 0 >= 655350) { + $9 = 6; + break label$1; + } + $8 = HEAP32[$0 >> 2]; + $10 = HEAP32[$8 + 36 >> 2]; + label$7 : { + if (!$10) { + $10 = HEAP32[$8 + 556 >> 2] ? 4096 : 1152; + HEAP32[$8 + 36 >> 2] = $10; + break label$7; + } + $9 = 7; + if ($10 + -16 >>> 0 > 65519) { + break label$1 + } + } + $9 = 8; + $11 = HEAP32[$8 + 556 >> 2]; + if ($11 >>> 0 > 32) { + break label$1 + } + $9 = 10; + if ($10 >>> 0 < $11 >>> 0) { + break label$1 + } + $11 = HEAP32[$8 + 560 >> 2]; + label$9 : { + if (!$11) { + $13 = $8; + $11 = HEAP32[$8 + 28 >> 2]; + label$11 : { + if ($11 >>> 0 <= 15) { + $11 = $11 >>> 0 > 5 ? ($11 >>> 1 | 0) + 2 | 0 : 5; + break label$11; + } + if (($11 | 0) == 16) { + $11 = 7; + if ($10 >>> 0 < 193) { + break label$11 + } + $11 = 8; + if ($10 >>> 0 < 385) { + break label$11 + } + $11 = 9; + if ($10 >>> 0 < 577) { + break label$11 + } + $11 = 10; + if ($10 >>> 0 < 1153) { + break label$11 + } + $11 = 11; + if ($10 >>> 0 < 2305) { + break label$11 + } + $11 = $10 >>> 0 < 4609 ? 12 : 13; + break label$11; + } + $11 = 13; + if ($10 >>> 0 < 385) { + break label$11 + } + $11 = $10 >>> 0 < 1153 ? 14 : 15; + } + HEAP32[$13 + 560 >> 2] = $11; + break label$9; + } + $9 = 9; + if ($11 + -5 >>> 0 > 10) { + break label$1 + } + } + label$14 : { + if (!HEAP32[$8 + 8 >> 2]) { + $10 = HEAP32[$8 + 580 >> 2]; + break label$14; + } + $9 = 11; + if (!(($10 >>> 0 < 4609 | HEAPU32[$8 + 32 >> 2] > 48e3) & $10 >>> 0 < 16385)) { + break label$1 + } + if (!FLAC__format_sample_rate_is_subset(HEAP32[HEAP32[$0 >> 2] + 32 >> 2])) { + break label$1 + } + $8 = HEAP32[$0 >> 2]; + if (__wasm_rotl_i32(HEAP32[$8 + 28 >> 2] + -8 | 0, 30) >>> 0 > 4) { + break label$1 + } + $10 = HEAP32[$8 + 580 >> 2]; + if ($10 >>> 0 > 8) { + break label$1 + } + if (HEAPU32[$8 + 32 >> 2] > 48e3) { + break label$14 + } + if (HEAPU32[$8 + 36 >> 2] > 4608 | HEAPU32[$8 + 556 >> 2] > 12) { + break label$1 + } + } + $11 = 1 << HEAP32[1406]; + if ($10 >>> 0 >= $11 >>> 0) { + $10 = $11 + -1 | 0; + HEAP32[$8 + 580 >> 2] = $10; + } + if (HEAPU32[$8 + 576 >> 2] >= $10 >>> 0) { + HEAP32[$8 + 576 >> 2] = $10 + } + label$18 : { + if (!$7) { + break label$18 + } + $10 = HEAP32[$8 + 600 >> 2]; + if (!$10) { + break label$18 + } + $13 = HEAP32[$8 + 604 >> 2]; + if ($13 >>> 0 < 2) { + break label$18 + } + $9 = 1; + while (1) { + $11 = HEAP32[($9 << 2) + $10 >> 2]; + if (!(!$11 | HEAP32[$11 >> 2] != 4)) { + while (1) { + $8 = ($9 << 2) + $10 | 0; + $9 = $9 + -1 | 0; + HEAP32[$8 >> 2] = HEAP32[($9 << 2) + $10 >> 2]; + $10 = HEAP32[HEAP32[$0 >> 2] + 600 >> 2]; + if ($9) { + continue + } + break; + }; + HEAP32[$10 >> 2] = $11; + $8 = HEAP32[$0 >> 2]; + break label$18; + } + $9 = $9 + 1 | 0; + if (($13 | 0) != ($9 | 0)) { + continue + } + break; + }; + } + $13 = HEAP32[$8 + 604 >> 2]; + label$22 : { + label$23 : { + $10 = HEAP32[$8 + 600 >> 2]; + if ($10) { + $11 = 0; + if (!$13) { + break label$22 + } + while (1) { + $8 = HEAP32[($11 << 2) + $10 >> 2]; + if (!(!$8 | HEAP32[$8 >> 2] != 3)) { + HEAP32[HEAP32[$0 + 4 >> 2] + 7048 >> 2] = $8 + 16; + break label$23; + } + $11 = $11 + 1 | 0; + if (($13 | 0) != ($11 | 0)) { + continue + } + break; + }; + break label$23; + } + $9 = 12; + if ($13) { + break label$1 + } + $11 = 0; + break label$22; + } + $8 = 0; + $13 = 0; + $11 = 0; + while (1) { + $9 = 12; + label$28 : { + label$29 : { + label$30 : { + label$31 : { + label$32 : { + $10 = HEAP32[($14 << 2) + $10 >> 2]; + switch (HEAP32[$10 >> 2]) { + case 0: + break label$1; + case 6: + break label$29; + case 5: + break label$30; + case 4: + break label$31; + case 3: + break label$32; + default: + break label$28; + }; + } + if ($18) { + break label$1 + } + $18 = 1; + $11 = $13; + $12 = $8; + if (FLAC__format_seektable_is_legal($10 + 16 | 0)) { + break label$28 + } + break label$1; + } + $11 = 1; + $12 = $8; + if (!$13) { + break label$28 + } + break label$1; + } + $11 = $13; + $12 = $8; + if (FLAC__format_cuesheet_is_legal($10 + 16 | 0, HEAP32[$10 + 160 >> 2])) { + break label$28 + } + break label$1; + } + $17 = $10 + 16 | 0; + if (!FLAC__format_picture_is_legal($17)) { + break label$1 + } + $11 = $13; + $12 = $8; + label$33 : { + switch (HEAP32[$17 >> 2] + -1 | 0) { + case 0: + if ($19) { + break label$1 + } + $12 = HEAP32[$10 + 20 >> 2]; + if (strcmp($12, 10763)) { + if (strcmp($12, 10773)) { + break label$1 + } + } + if (HEAP32[$10 + 28 >> 2] != 32) { + break label$1 + } + $19 = 1; + $11 = $13; + $12 = $8; + if (HEAP32[$10 + 32 >> 2] == 32) { + break label$28 + } + break label$1; + case 1: + break label$33; + default: + break label$28; + }; + } + $12 = 1; + if ($8) { + break label$1 + } + } + $14 = $14 + 1 | 0; + $8 = HEAP32[$0 >> 2]; + if ($14 >>> 0 >= HEAPU32[$8 + 604 >> 2]) { + break label$22 + } + $10 = HEAP32[$8 + 600 >> 2]; + $8 = $12; + $13 = $11; + continue; + }; + } + $10 = 0; + $14 = HEAP32[$0 + 4 >> 2]; + HEAP32[$14 >> 2] = 0; + if (HEAP32[$8 + 24 >> 2]) { + while (1) { + $8 = $10 << 2; + HEAP32[($8 + $14 | 0) + 4 >> 2] = 0; + HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 7328 >> 2] = 0; + HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 44 >> 2] = 0; + HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 7368 >> 2] = 0; + $14 = HEAP32[$0 + 4 >> 2]; + $10 = $10 + 1 | 0; + if ($10 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + } + } + $8 = 0; + HEAP32[$14 + 36 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7360 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 76 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7400 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 40 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7364 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 80 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7404 >> 2] = 0; + $9 = HEAP32[$0 + 4 >> 2]; + $10 = HEAP32[$0 >> 2]; + if (HEAP32[$10 + 40 >> 2]) { + while (1) { + $12 = $8 << 2; + HEAP32[($12 + $9 | 0) + 84 >> 2] = 0; + HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7408 >> 2] = 0; + $9 = HEAP32[$0 + 4 >> 2]; + $8 = $8 + 1 | 0; + $10 = HEAP32[$0 >> 2]; + if ($8 >>> 0 < HEAPU32[$10 + 40 >> 2]) { + continue + } + break; + } + } + $8 = 0; + HEAP32[$9 + 7536 >> 2] = 0; + HEAP32[$9 + 212 >> 2] = 0; + if (HEAP32[$10 + 24 >> 2]) { + while (1) { + $12 = $8 << 3; + HEAP32[($12 + $9 | 0) + 256 >> 2] = 0; + HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7540 >> 2] = 0; + HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 260 >> 2] = 0; + HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7544 >> 2] = 0; + $9 = HEAP32[$0 + 4 >> 2]; + HEAP32[($9 + ($8 << 2) | 0) + 6768 >> 2] = 0; + $8 = $8 + 1 | 0; + if ($8 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + } + } + HEAP32[$9 + 320 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7604 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 324 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7608 >> 2] = 0; + $8 = HEAP32[$0 + 4 >> 2]; + HEAP32[$8 + 6800 >> 2] = 0; + HEAP32[$8 + 328 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7612 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 332 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 7616 >> 2] = 0; + $8 = HEAP32[$0 + 4 >> 2]; + HEAP32[$8 + 7620 >> 2] = 0; + HEAP32[$8 + 7624 >> 2] = 0; + HEAP32[$8 + 6848 >> 2] = 0; + HEAP32[$8 + 6852 >> 2] = 0; + HEAP32[$8 + 6804 >> 2] = 0; + $12 = HEAP32[$0 >> 2]; + $13 = HEAP32[$12 + 36 >> 2]; + $12 = HEAP32[$12 + 32 >> 2]; + HEAP32[$8 + 7052 >> 2] = 0; + HEAP32[$8 + 7056 >> 2] = 0; + HEAP32[$8 + 6864 >> 2] = 0; + $9 = $8; + $16 = +($12 >>> 0) * .4 / +($13 >>> 0) + .5; + label$42 : { + if ($16 < 4294967296.0 & $16 >= 0.0) { + $12 = ~~$16 >>> 0; + break label$42; + } + $12 = 0; + } + HEAP32[$9 + 6860 >> 2] = $12 ? $12 : 1; + FLAC__cpu_info($8 + 7156 | 0); + $9 = HEAP32[$0 + 4 >> 2]; + HEAP32[$9 + 7244 >> 2] = 12; + HEAP32[$9 + 7240 >> 2] = 13; + HEAP32[$9 + 7236 >> 2] = 12; + HEAP32[$9 + 7228 >> 2] = 14; + HEAP32[$9 + 7224 >> 2] = 15; + HEAP32[$9 + 7220 >> 2] = 16; + HEAP32[$9 + 7232 >> 2] = 17; + $10 = HEAP32[$0 >> 2]; + HEAP32[$10 >> 2] = 0; + HEAP32[$9 + 7260 >> 2] = $7; + label$44 : { + label$45 : { + label$46 : { + if ($7) { + if (!FLAC__ogg_encoder_aspect_init($10 + 632 | 0)) { + break label$46 + } + $10 = HEAP32[$0 >> 2]; + $9 = HEAP32[$0 + 4 >> 2]; + } + $8 = $0 + 4 | 0; + HEAP32[$9 + 7276 >> 2] = $2; + HEAP32[$9 + 7264 >> 2] = $1; + HEAP32[$9 + 7288 >> 2] = $6; + HEAP32[$9 + 7280 >> 2] = $5; + HEAP32[$9 + 7272 >> 2] = $4; + HEAP32[$9 + 7268 >> 2] = $3; + $1 = HEAP32[$10 + 36 >> 2]; + if (HEAPU32[$9 >> 2] < $1 >>> 0) { + $3 = $1 + 5 | 0; + label$49 : { + label$50 : { + label$51 : { + if (HEAP32[$10 + 24 >> 2]) { + $2 = 0; + while (1) { + $5 = $2 << 2; + $4 = $5 + HEAP32[$8 >> 2] | 0; + $6 = FLAC__memory_alloc_aligned_int32_array($3, $4 + 7328 | 0, $4 + 4 | 0); + $4 = HEAP32[($5 + HEAP32[$8 >> 2] | 0) + 4 >> 2]; + HEAP32[$4 >> 2] = 0; + HEAP32[$4 + 4 >> 2] = 0; + HEAP32[$4 + 8 >> 2] = 0; + HEAP32[$4 + 12 >> 2] = 0; + $4 = ($5 + HEAP32[$8 >> 2] | 0) + 4 | 0; + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + 16; + if (!$6) { + break label$51 + } + $2 = $2 + 1 | 0; + if ($2 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + continue + } + break; + }; + } + $2 = HEAP32[$8 >> 2]; + $4 = FLAC__memory_alloc_aligned_int32_array($3, $2 + 7360 | 0, $2 + 36 | 0); + $2 = HEAP32[HEAP32[$8 >> 2] + 36 >> 2]; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + HEAP32[$2 + 8 >> 2] = 0; + HEAP32[$2 + 12 >> 2] = 0; + $2 = HEAP32[$8 >> 2]; + HEAP32[$2 + 36 >> 2] = HEAP32[$2 + 36 >> 2] + 16; + if ($4) { + $2 = HEAP32[$8 >> 2]; + $3 = FLAC__memory_alloc_aligned_int32_array($3, $2 + 7364 | 0, $2 + 40 | 0); + $2 = HEAP32[HEAP32[$8 >> 2] + 40 >> 2]; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + HEAP32[$2 + 8 >> 2] = 0; + HEAP32[$2 + 12 >> 2] = 0; + $2 = HEAP32[$8 >> 2] + 40 | 0; + HEAP32[$2 >> 2] = HEAP32[$2 >> 2] + 16; + $2 = ($3 | 0) != 0; + } else { + $2 = ($4 | 0) != 0 + } + if (!$2) { + break label$51 + } + $3 = HEAP32[$0 >> 2]; + if (HEAP32[$3 + 556 >> 2]) { + $2 = HEAP32[$8 >> 2]; + if (HEAP32[$3 + 40 >> 2]) { + $9 = 0; + while (1) { + $2 = ($9 << 2) + $2 | 0; + if (!FLAC__memory_alloc_aligned_int32_array($1, $2 + 7408 | 0, $2 + 84 | 0)) { + break label$51 + } + $2 = HEAP32[$0 + 4 >> 2]; + $9 = $9 + 1 | 0; + if ($9 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 40 >> 2]) { + continue + } + break; + }; + } + if (!FLAC__memory_alloc_aligned_int32_array($1, $2 + 7536 | 0, $2 + 212 | 0)) { + break label$51 + } + } + $6 = 0; + $10 = 1; + $5 = 0; + while (1) { + if ($5 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { + $9 = 0; + $2 = 1; + $3 = 0; + while (1) { + if ($9 & 1) { + break label$51 + } + $3 = (HEAP32[$8 >> 2] + ($5 << 3) | 0) + ($3 << 2) | 0; + $4 = FLAC__memory_alloc_aligned_int32_array($1, $3 + 7540 | 0, $3 + 256 | 0); + $7 = $2 & ($4 | 0) != 0; + $9 = !$4; + $3 = 1; + $2 = 0; + if ($7) { + continue + } + break; + }; + $5 = $5 + 1 | 0; + if ($4) { + continue + } + break label$51; + } + break; + }; + $7 = 1; + while (1) { + $9 = 0; + $2 = 1; + $3 = 0; + if (!$7) { + break label$51 + } + while (1) { + if ($9 & 1) { + break label$51 + } + $3 = (HEAP32[$8 >> 2] + ($6 << 3) | 0) + ($3 << 2) | 0; + $4 = FLAC__memory_alloc_aligned_int32_array($1, $3 + 7604 | 0, $3 + 320 | 0); + $5 = $2 & ($4 | 0) != 0; + $9 = !$4; + $3 = 1; + $2 = 0; + if ($5) { + continue + } + break; + }; + $7 = ($4 | 0) != 0; + $2 = $10 & $7; + $6 = 1; + $10 = 0; + if ($2) { + continue + } + break; + }; + if (!$4) { + break label$51 + } + $3 = $1 << 1; + $2 = HEAP32[$0 + 4 >> 2]; + $2 = FLAC__memory_alloc_aligned_uint64_array($3, $2 + 7620 | 0, $2 + 6848 | 0); + $9 = HEAP32[$0 >> 2]; + $4 = HEAP32[$9 + 572 >> 2]; + if (!$4 | !$2) { + break label$50 + } + $2 = HEAP32[$8 >> 2]; + if (FLAC__memory_alloc_aligned_int32_array($3, $2 + 7624 | 0, $2 + 6852 | 0)) { + break label$49 + } + } + $9 = HEAP32[$0 >> 2]; + break label$44; + } + if ($4 | !$2) { + break label$44 + } + } + $9 = HEAP32[$8 >> 2]; + label$64 : { + if (($1 | 0) == HEAP32[$9 >> 2]) { + break label$64 + } + $2 = HEAP32[$0 >> 2]; + if (!HEAP32[$2 + 556 >> 2] | !HEAP32[$2 + 40 >> 2]) { + break label$64 + } + $9 = 0; + while (1) { + label$66 : { + label$67 : { + label$68 : { + label$69 : { + label$70 : { + label$71 : { + label$72 : { + label$73 : { + label$74 : { + label$75 : { + label$76 : { + label$77 : { + label$78 : { + label$79 : { + label$80 : { + label$81 : { + label$82 : { + label$83 : { + label$84 : { + $2 = ($9 << 4) + $2 | 0; + switch (HEAP32[$2 + 44 >> 2]) { + case 16: + break label$68; + case 15: + break label$69; + case 14: + break label$70; + case 13: + break label$71; + case 12: + break label$72; + case 11: + break label$73; + case 10: + break label$74; + case 9: + break label$75; + case 8: + break label$76; + case 7: + break label$77; + case 6: + break label$78; + case 5: + break label$79; + case 4: + break label$80; + case 3: + break label$81; + case 2: + break label$82; + case 1: + break label$83; + case 0: + break label$84; + default: + break label$67; + }; + } + FLAC__window_bartlett(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_bartlett_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_blackman(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_blackman_harris_4term_92db_sidelobe(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_connes(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_flattop(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_gauss(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2]); + break label$66; + } + FLAC__window_hamming(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_kaiser_bessel(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_nuttall(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_rectangle(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_triangle(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2]); + break label$66; + } + FLAC__window_partial_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2], HEAPF32[$2 + 52 >> 2], HEAPF32[$2 + 56 >> 2]); + break label$66; + } + FLAC__window_punchout_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2], HEAPF32[$2 + 52 >> 2], HEAPF32[$2 + 56 >> 2]); + break label$66; + } + FLAC__window_welch(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + break label$66; + } + FLAC__window_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); + } + $9 = $9 + 1 | 0; + $2 = HEAP32[$0 >> 2]; + if ($9 >>> 0 < HEAPU32[$2 + 40 >> 2]) { + continue + } + break; + }; + $9 = HEAP32[$8 >> 2]; + } + HEAP32[$9 >> 2] = $1; + } + $1 = FLAC__bitwriter_init(HEAP32[$9 + 6856 >> 2]); + $3 = HEAP32[$0 >> 2]; + if (!$1) { + HEAP32[$3 >> 2] = 8; + $9 = 1; + break label$1; + } + if (HEAP32[$3 + 4 >> 2]) { + $9 = 1; + $2 = HEAP32[$8 >> 2]; + $1 = HEAP32[$3 + 36 >> 2] + 1 | 0; + HEAP32[$2 + 11796 >> 2] = $1; + label$87 : { + if (!HEAP32[$3 + 24 >> 2]) { + break label$87 + } + $1 = safe_malloc_mul_2op_p(4, $1); + HEAP32[HEAP32[$0 + 4 >> 2] + 11764 >> 2] = $1; + $3 = HEAP32[$0 >> 2]; + if ($1) { + while (1) { + $2 = HEAP32[$8 >> 2]; + if ($9 >>> 0 >= HEAPU32[$3 + 24 >> 2]) { + break label$87 + } + $1 = safe_malloc_mul_2op_p(4, HEAP32[$2 + 11796 >> 2]); + HEAP32[(HEAP32[$0 + 4 >> 2] + ($9 << 2) | 0) + 11764 >> 2] = $1; + $9 = $9 + 1 | 0; + $3 = HEAP32[$0 >> 2]; + if ($1) { + continue + } + break; + } + } + HEAP32[$3 >> 2] = 8; + $9 = 1; + break label$1; + } + HEAP32[$2 + 11800 >> 2] = 0; + label$90 : { + $2 = HEAP32[$2 + 11752 >> 2]; + if ($2) { + break label$90 + } + $2 = FLAC__stream_decoder_new(); + HEAP32[HEAP32[$8 >> 2] + 11752 >> 2] = $2; + if ($2) { + break label$90 + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 3; + $9 = 1; + break label$1; + } + $1 = FLAC__stream_decoder_init_stream($2, 18, 0, 0, 0, 0, 19, 20, 21, $0); + $3 = HEAP32[$0 >> 2]; + if ($1) { + break label$45 + } + $2 = !HEAP32[$3 + 4 >> 2]; + } else { + $2 = 1 + } + $1 = HEAP32[$8 >> 2]; + HEAP32[$1 + 7312 >> 2] = 0; + HEAP32[$1 + 7316 >> 2] = 0; + HEAP32[$1 + 7292 >> 2] = 0; + $4 = $1 + 11816 | 0; + HEAP32[$4 >> 2] = 0; + HEAP32[$4 + 4 >> 2] = 0; + $4 = $1 + 11824 | 0; + HEAP32[$4 >> 2] = 0; + HEAP32[$4 + 4 >> 2] = 0; + $4 = $1 + 11832 | 0; + HEAP32[$4 >> 2] = 0; + HEAP32[$4 + 4 >> 2] = 0; + HEAP32[$1 + 11840 >> 2] = 0; + HEAP32[$3 + 624 >> 2] = 0; + HEAP32[$3 + 628 >> 2] = 0; + HEAP32[$3 + 616 >> 2] = 0; + HEAP32[$3 + 620 >> 2] = 0; + HEAP32[$3 + 608 >> 2] = 0; + HEAP32[$3 + 612 >> 2] = 0; + if (!$2) { + HEAP32[$1 + 11756 >> 2] = 0 + } + if (!FLAC__bitwriter_write_raw_uint32(HEAP32[$1 + 6856 >> 2], HEAP32[1354], HEAP32[1355])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + $9 = 1; + break label$1; + } + $9 = 1; + if (!write_bitbuffer_($0, 0, 0)) { + break label$1 + } + $1 = HEAP32[$0 + 4 >> 2]; + $2 = HEAP32[$0 >> 2]; + if (HEAP32[$2 + 4 >> 2]) { + HEAP32[$1 + 11756 >> 2] = 1 + } + HEAP32[$1 + 6872 >> 2] = 0; + HEAP32[$1 + 6876 >> 2] = 0; + HEAP32[$1 + 6880 >> 2] = 34; + HEAP32[$1 + 6888 >> 2] = HEAP32[$2 + 36 >> 2]; + HEAP32[HEAP32[$0 + 4 >> 2] + 6892 >> 2] = HEAP32[HEAP32[$0 >> 2] + 36 >> 2]; + HEAP32[HEAP32[$0 + 4 >> 2] + 6896 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 6900 >> 2] = 0; + HEAP32[HEAP32[$0 + 4 >> 2] + 6904 >> 2] = HEAP32[HEAP32[$0 >> 2] + 32 >> 2]; + HEAP32[HEAP32[$0 + 4 >> 2] + 6908 >> 2] = HEAP32[HEAP32[$0 >> 2] + 24 >> 2]; + HEAP32[HEAP32[$0 + 4 >> 2] + 6912 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; + $1 = HEAP32[$0 >> 2]; + $2 = HEAP32[$1 + 596 >> 2]; + $3 = HEAP32[$0 + 4 >> 2] + 6920 | 0; + HEAP32[$3 >> 2] = HEAP32[$1 + 592 >> 2]; + HEAP32[$3 + 4 >> 2] = $2; + $1 = HEAP32[$0 + 4 >> 2]; + $2 = $1 + 6936 | 0; + HEAP32[$2 >> 2] = 0; + HEAP32[$2 + 4 >> 2] = 0; + $1 = $1 + 6928 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + if (HEAP32[HEAP32[$0 >> 2] + 12 >> 2]) { + FLAC__MD5Init(HEAP32[$8 >> 2] + 7060 | 0) + } + $1 = HEAP32[$8 >> 2]; + if (!FLAC__add_metadata_block($1 + 6872 | 0, HEAP32[$1 + 6856 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + break label$1; + } + if (!write_bitbuffer_($0, 0, 0)) { + break label$1 + } + HEAP32[HEAP32[$8 >> 2] + 6896 >> 2] = -1 << HEAP32[1358] ^ -1; + $1 = HEAP32[$8 >> 2] + 6920 | 0; + HEAP32[$1 >> 2] = 0; + HEAP32[$1 + 4 >> 2] = 0; + if (!$11) { + HEAP32[$15 >> 2] = 4; + $2 = HEAP32[HEAP32[$0 >> 2] + 604 >> 2]; + $1 = $15; + HEAP32[$1 + 24 >> 2] = 0; + HEAP32[$1 + 28 >> 2] = 0; + HEAP32[$1 + 16 >> 2] = 0; + HEAP32[$1 + 20 >> 2] = 0; + HEAP32[$1 + 8 >> 2] = 8; + HEAP32[$1 + 4 >> 2] = !$2; + if (!FLAC__add_metadata_block($1, HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + break label$1; + } + if (!write_bitbuffer_($0, 0, 0)) { + break label$1 + } + } + label$98 : { + $3 = HEAP32[$0 >> 2]; + $4 = HEAP32[$3 + 604 >> 2]; + if (!$4) { + break label$98 + } + $2 = 0; + while (1) { + $1 = HEAP32[HEAP32[$3 + 600 >> 2] + ($2 << 2) >> 2]; + HEAP32[$1 + 4 >> 2] = ($4 + -1 | 0) == ($2 | 0); + if (!FLAC__add_metadata_block($1, HEAP32[HEAP32[$8 >> 2] + 6856 >> 2])) { + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + break label$1; + } + if (write_bitbuffer_($0, 0, 0)) { + $2 = $2 + 1 | 0; + $3 = HEAP32[$0 >> 2]; + $4 = HEAP32[$3 + 604 >> 2]; + if ($2 >>> 0 >= $4 >>> 0) { + break label$98 + } + continue; + } + break; + }; + break label$1; + } + label$102 : { + $1 = HEAP32[$8 >> 2]; + $2 = HEAP32[$1 + 7272 >> 2]; + if (!$2) { + break label$102 + } + $1 = FUNCTION_TABLE[$2]($0, $3 + 624 | 0, HEAP32[$1 + 7288 >> 2]) | 0; + $3 = HEAP32[$0 >> 2]; + if (($1 | 0) != 1) { + break label$102 + } + HEAP32[$3 >> 2] = 5; + break label$1; + } + $9 = 0; + if (!HEAP32[$3 + 4 >> 2]) { + break label$1 + } + HEAP32[HEAP32[$8 >> 2] + 11756 >> 2] = 2; + break label$1; + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 2; + $9 = 1; + break label$1; + } + HEAP32[$3 >> 2] = 3; + $9 = 1; + break label$1; + } + HEAP32[$9 >> 2] = 8; + $9 = 1; + } + global$0 = $15 + 176 | 0; + return $9; + } + + function precompute_partition_info_sums_($0, $1, $2, $3, $4, $5, $6) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + var $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; + $11 = 1 << $5; + $14 = $11 >>> 0 > 1 ? $11 : 1; + $8 = 0 - $3 | 0; + $12 = $2 + $3 >>> $5 | 0; + $9 = $12 - $3 | 0; + label$1 : { + if ($6 + 4 >>> 0 < (Math_clz32($12) ^ -32) + 33 >>> 0) { + $6 = 0; + while (1) { + $3 = 0; + $8 = $8 + $12 | 0; + if ($7 >>> 0 < $8 >>> 0) { + while (1) { + $2 = HEAP32[($7 << 2) + $0 >> 2]; + $10 = $2 >> 31; + $3 = ($10 ^ $2 + $10) + $3 | 0; + $7 = $7 + 1 | 0; + if ($7 >>> 0 < $8 >>> 0) { + continue + } + break; + }; + $7 = $9; + } + $2 = ($6 << 3) + $1 | 0; + HEAP32[$2 >> 2] = $3; + HEAP32[$2 + 4 >> 2] = 0; + $9 = $9 + $12 | 0; + $6 = $6 + 1 | 0; + if (($14 | 0) != ($6 | 0)) { + continue + } + break; + }; + break label$1; + } + $2 = 0; + while (1) { + $13 = 0; + $3 = 0; + $8 = $8 + $12 | 0; + if ($7 >>> 0 < $8 >>> 0) { + while (1) { + $6 = HEAP32[($7 << 2) + $0 >> 2]; + $10 = $6 >> 31; + $10 = $10 ^ $6 + $10; + $6 = $10 + $13 | 0; + if ($6 >>> 0 < $10 >>> 0) { + $3 = $3 + 1 | 0 + } + $13 = $6; + $7 = $7 + 1 | 0; + if ($7 >>> 0 < $8 >>> 0) { + continue + } + break; + }; + $7 = $9; + } + $6 = ($2 << 3) + $1 | 0; + HEAP32[$6 >> 2] = $13; + HEAP32[$6 + 4 >> 2] = $3; + $9 = $9 + $12 | 0; + $2 = $2 + 1 | 0; + if (($14 | 0) != ($2 | 0)) { + continue + } + break; + }; + } + if (($5 | 0) > ($4 | 0)) { + $7 = 0; + $0 = $11; + while (1) { + $5 = $5 + -1 | 0; + $8 = 0; + $0 = $0 >>> 1 | 0; + if ($0) { + while (1) { + $3 = ($7 << 3) + $1 | 0; + $2 = HEAP32[$3 + 8 >> 2]; + $9 = HEAP32[$3 + 12 >> 2] + HEAP32[$3 + 4 >> 2] | 0; + $3 = HEAP32[$3 >> 2]; + $2 = $3 + $2 | 0; + if ($2 >>> 0 < $3 >>> 0) { + $9 = $9 + 1 | 0 + } + $6 = ($11 << 3) + $1 | 0; + HEAP32[$6 >> 2] = $2; + HEAP32[$6 + 4 >> 2] = $9; + $7 = $7 + 2 | 0; + $11 = $11 + 1 | 0; + $8 = $8 + 1 | 0; + if (($8 | 0) != ($0 | 0)) { + continue + } + break; + } + } + if (($5 | 0) > ($4 | 0)) { + continue + } + break; + }; + } + } + + function verify_read_callback_($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + var $4 = 0, $5 = 0; + $5 = HEAP32[$3 + 4 >> 2]; + if (HEAP32[$5 + 11760 >> 2]) { + HEAP32[$2 >> 2] = 4; + $0 = HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24); + HEAP8[$1 | 0] = $0; + HEAP8[$1 + 1 | 0] = $0 >>> 8; + HEAP8[$1 + 2 | 0] = $0 >>> 16; + HEAP8[$1 + 3 | 0] = $0 >>> 24; + HEAP32[HEAP32[$3 + 4 >> 2] + 11760 >> 2] = 0; + return 0; + } + $0 = HEAP32[$5 + 11812 >> 2]; + if (!$0) { + return 2 + } + $4 = HEAP32[$2 >> 2]; + if ($0 >>> 0 < $4 >>> 0) { + HEAP32[$2 >> 2] = $0; + $4 = $0; + } + memcpy($1, HEAP32[$5 + 11804 >> 2], $4); + $0 = HEAP32[$3 + 4 >> 2]; + $1 = $0 + 11804 | 0; + $3 = $1; + $4 = HEAP32[$1 >> 2]; + $1 = HEAP32[$2 >> 2]; + HEAP32[$3 >> 2] = $4 + $1; + $0 = $0 + 11812 | 0; + HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - $1; + return 0; + } + + function verify_write_callback_($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; + $7 = HEAP32[$1 >> 2]; + $5 = HEAP32[$3 + 4 >> 2]; + $0 = HEAP32[$1 + 8 >> 2]; + if ($0) { + $4 = $7 << 2; + while (1) { + $8 = $6 << 2; + $9 = HEAP32[$8 + $2 >> 2]; + $10 = HEAP32[($5 + $8 | 0) + 11764 >> 2]; + if (memcmp($9, $10, $4)) { + $4 = 0; + label$4 : { + if ($7) { + $0 = 0; + while (1) { + $2 = $0 << 2; + $8 = HEAP32[$2 + $9 >> 2]; + $2 = HEAP32[$2 + $10 >> 2]; + if (($8 | 0) != ($2 | 0)) { + $4 = $0; + break label$4; + } + $0 = $0 + 1 | 0; + if (($7 | 0) != ($0 | 0)) { + continue + } + break; + }; + } + $2 = 0; + $8 = 0; + } + $9 = HEAP32[$1 + 28 >> 2]; + $0 = $4; + $11 = $0 + HEAP32[$1 + 24 >> 2] | 0; + if ($11 >>> 0 < $0 >>> 0) { + $9 = $9 + 1 | 0 + } + $10 = $5 + 11816 | 0; + HEAP32[$10 >> 2] = $11; + HEAP32[$10 + 4 >> 2] = $9; + $0 = HEAP32[$1 + 28 >> 2]; + $1 = HEAP32[$1 + 24 >> 2]; + HEAP32[$5 + 11840 >> 2] = $8; + HEAP32[$5 + 11836 >> 2] = $2; + HEAP32[$5 + 11832 >> 2] = $4; + HEAP32[$5 + 11828 >> 2] = $6; + (wasm2js_i32$0 = $5 + 11824 | 0, wasm2js_i32$1 = __wasm_i64_udiv($1, $0, $7)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; + HEAP32[HEAP32[$3 >> 2] >> 2] = 4; + return 1; + } + $6 = $6 + 1 | 0; + if (($0 | 0) != ($6 | 0)) { + continue + } + break; + }; + $2 = $5 + 11800 | 0; + $1 = HEAP32[$2 >> 2] - $7 | 0; + HEAP32[$2 >> 2] = $1; + label$8 : { + if (!$0) { + break label$8 + } + $2 = HEAP32[$5 + 11764 >> 2]; + $4 = $2; + $2 = $7 << 2; + memmove($4, $4 + $2 | 0, $1 << 2); + $6 = 1; + if (($0 | 0) == 1) { + break label$8 + } + while (1) { + $1 = HEAP32[$3 + 4 >> 2]; + $4 = HEAP32[($1 + ($6 << 2) | 0) + 11764 >> 2]; + memmove($4, $2 + $4 | 0, HEAP32[$1 + 11800 >> 2] << 2); + $6 = $6 + 1 | 0; + if (($0 | 0) != ($6 | 0)) { + continue + } + break; + }; + } + return 0; + } + $0 = $5 + 11800 | 0; + HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - $7; + return 0; + } + + function verify_metadata_callback_($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + } + + function verify_error_callback_($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + HEAP32[HEAP32[$2 >> 2] >> 2] = 3; + } + + function write_bitbuffer_($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0; + $5 = global$0 - 16 | 0; + global$0 = $5; + $4 = FLAC__bitwriter_get_buffer(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], $5 + 4 | 0, $5); + $3 = HEAP32[$0 >> 2]; + label$1 : { + label$2 : { + if (!$4) { + HEAP32[$3 >> 2] = 8; + break label$2; + } + label$4 : { + if (!HEAP32[$3 + 4 >> 2]) { + break label$4 + } + $3 = HEAP32[$0 + 4 >> 2]; + HEAP32[$3 + 11804 >> 2] = HEAP32[$5 + 4 >> 2]; + HEAP32[$3 + 11812 >> 2] = HEAP32[$5 >> 2]; + if (!HEAP32[$3 + 11756 >> 2]) { + HEAP32[$3 + 11760 >> 2] = 1; + break label$4; + } + if (FLAC__stream_decoder_process_single(HEAP32[$3 + 11752 >> 2])) { + break label$4 + } + FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 4) { + break label$1 + } + HEAP32[$0 >> 2] = 3; + break label$1; + } + $12 = HEAP32[$5 >> 2]; + $14 = HEAP32[$5 + 4 >> 2]; + HEAP32[$5 + 8 >> 2] = 0; + HEAP32[$5 + 12 >> 2] = 0; + label$6 : { + label$7 : { + $3 = HEAP32[$0 + 4 >> 2]; + $4 = HEAP32[$3 + 7272 >> 2]; + if (!$4) { + break label$7 + } + if ((FUNCTION_TABLE[$4]($0, $5 + 8 | 0, HEAP32[$3 + 7288 >> 2]) | 0) != 1) { + break label$7 + } + break label$6; + } + label$8 : { + if ($1) { + break label$8 + } + label$9 : { + switch (HEAPU8[$14 | 0] & 127) { + case 0: + $3 = HEAP32[$5 + 12 >> 2]; + $4 = HEAP32[$0 >> 2]; + HEAP32[$4 + 608 >> 2] = HEAP32[$5 + 8 >> 2]; + HEAP32[$4 + 612 >> 2] = $3; + break label$8; + case 3: + break label$9; + default: + break label$8; + }; + } + $3 = HEAP32[$0 >> 2]; + if (HEAP32[$3 + 616 >> 2] | HEAP32[$3 + 620 >> 2]) { + break label$8 + } + $4 = HEAP32[$5 + 12 >> 2]; + HEAP32[$3 + 616 >> 2] = HEAP32[$5 + 8 >> 2]; + HEAP32[$3 + 620 >> 2] = $4; + } + $6 = HEAP32[$0 + 4 >> 2]; + $7 = HEAP32[$6 + 7048 >> 2]; + label$11 : { + if (!$7) { + break label$11 + } + $8 = HEAP32[$0 >> 2]; + $4 = $8; + $3 = HEAP32[$4 + 628 >> 2]; + $15 = HEAP32[$4 + 624 >> 2]; + if (!($3 | $15)) { + break label$11 + } + $16 = HEAP32[$7 >> 2]; + if (!$16) { + break label$11 + } + $10 = HEAP32[$6 + 7292 >> 2]; + if ($10 >>> 0 >= $16 >>> 0) { + break label$11 + } + $13 = HEAP32[$6 + 7316 >> 2]; + $4 = $13; + $17 = HEAP32[$6 + 7312 >> 2]; + $18 = HEAP32[$8 + 36 >> 2]; + $8 = $18; + $9 = $17 + $8 | 0; + if ($9 >>> 0 < $8 >>> 0) { + $4 = $4 + 1 | 0 + } + $4 = $4 + -1 | 0; + $11 = $4 + 1 | 0; + $8 = $4; + $4 = $9 + -1 | 0; + $8 = ($4 | 0) != -1 ? $11 : $8; + $19 = HEAP32[$7 + 4 >> 2]; + while (1) { + $7 = $19 + Math_imul($10, 24) | 0; + $11 = HEAP32[$7 >> 2]; + $9 = HEAP32[$7 + 4 >> 2]; + if (($8 | 0) == ($9 | 0) & $11 >>> 0 > $4 >>> 0 | $9 >>> 0 > $8 >>> 0) { + break label$11 + } + if (($9 | 0) == ($13 | 0) & $11 >>> 0 >= $17 >>> 0 | $9 >>> 0 > $13 >>> 0) { + HEAP32[$7 >> 2] = $17; + HEAP32[$7 + 4 >> 2] = $13; + $9 = HEAP32[$5 + 8 >> 2]; + $11 = HEAP32[$5 + 12 >> 2]; + HEAP32[$7 + 16 >> 2] = $18; + HEAP32[$7 + 8 >> 2] = $9 - $15; + HEAP32[$7 + 12 >> 2] = $11 - ($3 + ($9 >>> 0 < $15 >>> 0) | 0); + } + $10 = $10 + 1 | 0; + HEAP32[$6 + 7292 >> 2] = $10; + if (($10 | 0) != ($16 | 0)) { + continue + } + break; + }; + } + label$14 : { + if (HEAP32[$6 + 7260 >> 2]) { + $2 = FLAC__ogg_encoder_aspect_write_callback_wrapper(HEAP32[$0 >> 2] + 632 | 0, $14, $12, $1, HEAP32[$6 + 7056 >> 2], $2, HEAP32[$6 + 7276 >> 2], $0, HEAP32[$6 + 7288 >> 2]); + break label$14; + } + $2 = FUNCTION_TABLE[HEAP32[$6 + 7276 >> 2]]($0, $14, $12, $1, HEAP32[$6 + 7056 >> 2], HEAP32[$6 + 7288 >> 2]) | 0; + } + if (!$2) { + $2 = HEAP32[$0 + 4 >> 2]; + $3 = $2; + $8 = $3; + $4 = HEAP32[$3 + 7308 >> 2]; + $6 = $12 + HEAP32[$3 + 7304 >> 2] | 0; + if ($6 >>> 0 < $12 >>> 0) { + $4 = $4 + 1 | 0 + } + HEAP32[$8 + 7304 >> 2] = $6; + HEAP32[$3 + 7308 >> 2] = $4; + $3 = HEAP32[$2 + 7316 >> 2]; + $4 = HEAP32[$2 + 7312 >> 2] + $1 | 0; + if ($4 >>> 0 < $1 >>> 0) { + $3 = $3 + 1 | 0 + } + HEAP32[$2 + 7312 >> 2] = $4; + HEAP32[$2 + 7316 >> 2] = $3; + $10 = 1; + $4 = $2; + $3 = HEAP32[$2 + 7320 >> 2]; + $2 = HEAP32[$2 + 7056 >> 2] + 1 | 0; + HEAP32[$4 + 7320 >> 2] = $3 >>> 0 > $2 >>> 0 ? $3 : $2; + FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); + if (!$1) { + break label$1 + } + $1 = HEAP32[$0 + 4 >> 2] + 6896 | 0; + $2 = HEAP32[$1 >> 2]; + $4 = $1; + $1 = HEAP32[$5 >> 2]; + HEAP32[$4 >> 2] = $1 >>> 0 < $2 >>> 0 ? $1 : $2; + $2 = HEAP32[$0 + 4 >> 2] + 6900 | 0; + $0 = HEAP32[$2 >> 2]; + HEAP32[$2 >> 2] = $1 >>> 0 > $0 >>> 0 ? $1 : $0; + break label$1; + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); + HEAP32[HEAP32[$0 >> 2] >> 2] = 5; + } + $10 = 0; + } + global$0 = $5 + 16 | 0; + return $10; + } + + function FLAC__stream_encoder_init_ogg_stream($0, $1, $2, $3, $4, $5, $6) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + return init_stream_internal__1($0, $1, $2, $3, $4, $5, $6, 1) | 0; + } + + function process_subframe_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) { + var $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0.0, $25 = 0, $26 = 0, $27 = 0.0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = Math_fround(0), $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = Math_fround(0), $43 = 0, $44 = 0, $45 = 0, $46 = 0, $47 = 0, $48 = 0; + $14 = global$0 - 576 | 0; + global$0 = $14; + $25 = HEAP32[(HEAPU32[HEAP32[$0 >> 2] + 28 >> 2] > 16 ? 5644 : 5640) >> 2]; + $12 = HEAP32[$3 >> 2]; + label$1 : { + label$2 : { + if (HEAP32[HEAP32[$0 + 4 >> 2] + 7256 >> 2]) { + $11 = -1; + if ($12 >>> 0 > 3) { + break label$2 + } + } + $16 = HEAP32[$6 >> 2]; + HEAP32[$16 + 4 >> 2] = $5; + HEAP32[$16 >> 2] = 1; + $11 = HEAP32[$16 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + Math_imul($4, $12) | 0) | 0) | 0) | 0; + $12 = HEAP32[$3 >> 2]; + if ($12 >>> 0 < 4) { + break label$1 + } + } + $13 = HEAP32[$0 + 4 >> 2]; + $16 = $12 + -4 | 0; + label$4 : { + if (((Math_clz32($16 | 1) ^ 31) + $4 | 0) + 4 >>> 0 <= 32) { + $13 = FUNCTION_TABLE[HEAP32[$13 + 7224 >> 2]]($5 + 16 | 0, $16, $14 + 416 | 0) | 0; + break label$4; + } + $13 = FUNCTION_TABLE[HEAP32[$13 + 7228 >> 2]]($5 + 16 | 0, $16, $14 + 416 | 0) | 0; + } + label$6 : { + label$7 : { + label$8 : { + label$9 : { + $15 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$15 + 7248 >> 2] | HEAPF32[$14 + 420 >> 2] != Math_fround(0.0)) { + break label$9 + } + $12 = 1; + $17 = HEAP32[$5 >> 2]; + $16 = HEAP32[$3 >> 2]; + if ($16 >>> 0 <= 1) { + break label$8 + } + while (1) { + if (($17 | 0) != HEAP32[($12 << 2) + $5 >> 2]) { + break label$9 + } + $12 = $12 + 1 | 0; + if ($12 >>> 0 < $16 >>> 0) { + continue + } + break; + }; + break label$8; + } + $12 = HEAP32[$0 >> 2]; + if (!HEAP32[$15 + 7252 >> 2]) { + $16 = $11; + break label$7; + } + $16 = -1; + if (($11 | 0) != -1) { + $16 = $11; + break label$6; + } + if (!HEAP32[$12 + 556 >> 2]) { + break label$7 + } + $16 = $11; + break label$6; + } + $0 = HEAP32[$6 + 4 >> 2]; + HEAP32[$0 + 4 >> 2] = $17; + HEAP32[$0 >> 2] = 0; + $0 = HEAP32[$0 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + $4 | 0) | 0) | 0) | 0; + $19 = $0 >>> 0 < $11 >>> 0; + $11 = $19 ? $0 : $11; + break label$1; + } + $11 = HEAP32[$12 + 568 >> 2]; + $18 = $11 ? 0 : $13; + $13 = $11 ? 4 : $13; + $11 = HEAP32[$3 >> 2]; + $29 = $13 >>> 0 < $11 >>> 0 ? $13 : $11 + -1 | 0; + if ($18 >>> 0 > $29 >>> 0) { + break label$6 + } + $32 = $25 + -1 | 0; + $33 = HEAP32[1416]; + $30 = HEAP32[1415]; + $34 = HEAP32[1414]; + $42 = Math_fround($4 >>> 0); + while (1) { + $12 = $18 << 2; + $35 = HEAPF32[$12 + ($14 + 416 | 0) >> 2]; + if (!($35 >= $42)) { + $31 = !$19; + $17 = $31 << 2; + $36 = HEAP32[$17 + $7 >> 2]; + $21 = HEAP32[$6 + $17 >> 2]; + $23 = HEAP32[HEAP32[$0 >> 2] + 572 >> 2]; + $11 = HEAP32[$0 + 4 >> 2]; + $13 = HEAP32[$11 + 6852 >> 2]; + $15 = HEAP32[$11 + 6848 >> 2]; + $11 = $5 + $12 | 0; + $12 = HEAP32[$3 >> 2] - $18 | 0; + $17 = HEAP32[$8 + $17 >> 2]; + FLAC__fixed_compute_residual($11, $12, $18, $17); + HEAP32[$21 + 36 >> 2] = $17; + HEAP32[$21 + 12 >> 2] = $36; + HEAP32[$21 >> 2] = 2; + HEAP32[$21 + 4 >> 2] = 0; + $37 = $35 > Math_fround(0.0); + $26 = HEAP32[$0 + 4 >> 2]; + $22 = $18; + $27 = +$35 + .5; + label$15 : { + if ($27 < 4294967296.0 & $27 >= 0.0) { + $11 = ~~$27 >>> 0; + break label$15; + } + $11 = 0; + } + $11 = $37 ? $11 + 1 | 0 : 1; + $15 = find_best_partition_order_($26, $17, $15, $13, $12, $22, $11 >>> 0 < $25 >>> 0 ? $11 : $32, $25, $1, $2, $4, $23, $21 + 4 | 0); + HEAP32[$21 + 16 >> 2] = $18; + if ($18) { + $13 = $21 + 20 | 0; + $11 = 0; + while (1) { + $12 = $11 << 2; + HEAP32[$12 + $13 >> 2] = HEAP32[$5 + $12 >> 2]; + $11 = $11 + 1 | 0; + if (($18 | 0) != ($11 | 0)) { + continue + } + break; + }; + } + $11 = HEAP32[$21 + 288 >> 2] + ($33 + ($30 + ($34 + ($15 + Math_imul($4, $18) | 0) | 0) | 0) | 0) | 0; + $12 = $11 >>> 0 < $16 >>> 0; + $19 = $12 ? $31 : $19; + $16 = $12 ? $11 : $16; + } + $18 = $18 + 1 | 0; + if ($18 >>> 0 <= $29 >>> 0) { + continue + } + break; + }; + $12 = HEAP32[$0 >> 2]; + } + $13 = HEAP32[$12 + 556 >> 2]; + if (!$13) { + $11 = $16; + break label$1; + } + $11 = HEAP32[$3 >> 2]; + $13 = $13 >>> 0 < $11 >>> 0 ? $13 : $11 + -1 | 0; + HEAP32[$14 + 12 >> 2] = $13; + if (!$13) { + $11 = $16; + break label$1; + } + if (!HEAP32[$12 + 40 >> 2]) { + $11 = $16; + break label$1; + } + $40 = 33 - $4 | 0; + $43 = $25 + -1 | 0; + $44 = HEAP32[1413]; + $45 = HEAP32[1412]; + $46 = HEAP32[1416]; + $21 = HEAP32[1415]; + $47 = HEAP32[1414]; + $27 = +($4 >>> 0); + $29 = $4 >>> 0 < 18; + $32 = $4 >>> 0 > 16; + $33 = $4 >>> 0 > 17; + while (1) { + $12 = HEAP32[$0 + 4 >> 2]; + FLAC__lpc_window_data($5, HEAP32[($12 + ($38 << 2) | 0) + 84 >> 2], HEAP32[$12 + 212 >> 2], $11); + $11 = HEAP32[$0 + 4 >> 2]; + FUNCTION_TABLE[HEAP32[$11 + 7232 >> 2]](HEAP32[$11 + 212 >> 2], HEAP32[$3 >> 2], HEAP32[$14 + 12 >> 2] + 1 | 0, $14 + 272 | 0); + label$23 : { + if (HEAPF32[$14 + 272 >> 2] == Math_fround(0.0)) { + break label$23 + } + FLAC__lpc_compute_lp_coefficients($14 + 272 | 0, $14 + 12 | 0, HEAP32[$0 + 4 >> 2] + 7628 | 0, $14 + 16 | 0); + $15 = 1; + $12 = HEAP32[$14 + 12 >> 2]; + $17 = HEAP32[$0 >> 2]; + if (!HEAP32[$17 + 568 >> 2]) { + $11 = $14; + $12 = FLAC__lpc_compute_best_order($11 + 16 | 0, $12, HEAP32[$3 >> 2], (HEAP32[$17 + 564 >> 2] ? 5 : HEAP32[$17 + 560 >> 2]) + $4 | 0); + HEAP32[$11 + 12 >> 2] = $12; + $15 = $12; + } + $11 = HEAP32[$3 >> 2]; + if ($12 >>> 0 >= $11 >>> 0) { + $12 = $11 + -1 | 0; + HEAP32[$14 + 12 >> 2] = $12; + } + if ($15 >>> 0 > $12 >>> 0) { + break label$23 + } + while (1) { + label$29 : { + $30 = $15 + -1 | 0; + $24 = FLAC__lpc_compute_expected_bits_per_residual_sample(HEAPF64[($14 + 16 | 0) + ($30 << 3) >> 3], $11 - $15 | 0); + if ($24 >= $27) { + break label$29 + } + $11 = $24 > 0.0; + $24 = $24 + .5; + label$30 : { + if ($24 < 4294967296.0 & $24 >= 0.0) { + $13 = ~~$24 >>> 0; + break label$30; + } + $13 = 0; + } + $13 = $11 ? $13 + 1 | 0 : 1; + $11 = $13 >>> 0 < $25 >>> 0; + $12 = HEAP32[$0 >> 2]; + label$32 : { + if (HEAP32[$12 + 564 >> 2]) { + $22 = 5; + $26 = 15; + if ($33) { + break label$32 + } + $17 = (Math_clz32($15) ^ -32) + $40 | 0; + if ($17 >>> 0 > 14) { + break label$32 + } + $26 = $17 >>> 0 > 5 ? $17 : 5; + break label$32; + } + $26 = HEAP32[$12 + 560 >> 2]; + $22 = $26; + } + $34 = $11 ? $13 : $43; + $39 = ($15 << 2) + $5 | 0; + $11 = Math_clz32($15); + $31 = $11 ^ 31; + $41 = ($11 ^ -32) + $40 | 0; + while (1) { + $23 = HEAP32[$3 >> 2]; + $13 = !$19; + $11 = $13 << 2; + $37 = HEAP32[$11 + $7 >> 2]; + $20 = HEAP32[$6 + $11 >> 2]; + $28 = HEAP32[$8 + $11 >> 2]; + $36 = HEAP32[$12 + 572 >> 2]; + $12 = HEAP32[$0 + 4 >> 2]; + $18 = HEAP32[$12 + 6852 >> 2]; + $17 = HEAP32[$12 + 6848 >> 2]; + $11 = 0; + $48 = $19; + $19 = ($12 + ($30 << 7) | 0) + 7628 | 0; + $12 = $29 ? ($41 >>> 0 > $22 >>> 0 ? $22 : $41) : $22; + if (!FLAC__lpc_quantize_coefficients($19, $15, $12, $14 + 448 | 0, $14 + 444 | 0)) { + $23 = $23 - $15 | 0; + $19 = $4 + $12 | 0; + label$37 : { + if ($19 + $31 >>> 0 <= 32) { + $11 = HEAP32[$0 + 4 >> 2]; + if (!($12 >>> 0 > 16 | $32)) { + FUNCTION_TABLE[HEAP32[$11 + 7244 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); + break label$37; + } + FUNCTION_TABLE[HEAP32[$11 + 7236 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); + break label$37; + } + FUNCTION_TABLE[HEAP32[HEAP32[$0 + 4 >> 2] + 7240 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); + } + HEAP32[$20 >> 2] = 3; + HEAP32[$20 + 4 >> 2] = 0; + HEAP32[$20 + 284 >> 2] = $28; + HEAP32[$20 + 12 >> 2] = $37; + $18 = find_best_partition_order_(HEAP32[$0 + 4 >> 2], $28, $17, $18, $23, $15, $34, $25, $1, $2, $4, $36, $20 + 4 | 0); + HEAP32[$20 + 20 >> 2] = $12; + HEAP32[$20 + 16 >> 2] = $15; + HEAP32[$20 + 24 >> 2] = HEAP32[$14 + 444 >> 2]; + memcpy($20 + 28 | 0, $14 + 448 | 0, 128); + $11 = 0; + if ($15) { + while (1) { + $17 = $11 << 2; + HEAP32[($17 + $20 | 0) + 156 >> 2] = HEAP32[$5 + $17 >> 2]; + $11 = $11 + 1 | 0; + if (($15 | 0) != ($11 | 0)) { + continue + } + break; + } + } + $11 = ((HEAP32[$20 + 288 >> 2] + (((($18 + Math_imul($15, $19) | 0) + $47 | 0) + $21 | 0) + $46 | 0) | 0) + $45 | 0) + $44 | 0; + } + $12 = ($11 | 0) != 0 & $11 >>> 0 < $16 >>> 0; + $19 = $12 ? $13 : $48; + $16 = $12 ? $11 : $16; + $22 = $22 + 1 | 0; + if ($22 >>> 0 > $26 >>> 0) { + break label$29 + } + $12 = HEAP32[$0 >> 2]; + continue; + }; + } + $15 = $15 + 1 | 0; + if ($15 >>> 0 > HEAPU32[$14 + 12 >> 2]) { + break label$23 + } + $11 = HEAP32[$3 >> 2]; + continue; + }; + } + $38 = $38 + 1 | 0; + if ($38 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 40 >> 2]) { + $11 = HEAP32[$3 >> 2]; + continue; + } + break; + }; + $11 = $16; + } + if (($11 | 0) == -1) { + $0 = HEAP32[$3 >> 2]; + $1 = HEAP32[($19 << 2) + $6 >> 2]; + HEAP32[$1 + 4 >> 2] = $5; + HEAP32[$1 >> 2] = 1; + $11 = HEAP32[$1 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + Math_imul($0, $4) | 0) | 0) | 0) | 0; + } + HEAP32[$9 >> 2] = $19; + HEAP32[$10 >> 2] = $11; + global$0 = $14 + 576 | 0; + } + + function add_subframe_($0, $1, $2, $3, $4) { + var $5 = 0; + $5 = 1; + label$1 : { + label$2 : { + label$3 : { + switch (HEAP32[$3 >> 2]) { + case 0: + if (FLAC__subframe_add_constant($3 + 4 | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { + break label$1 + } + break label$2; + case 2: + if (FLAC__subframe_add_fixed($3 + 4 | 0, $1 - HEAP32[$3 + 16 >> 2] | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { + break label$1 + } + break label$2; + case 3: + if (FLAC__subframe_add_lpc($3 + 4 | 0, $1 - HEAP32[$3 + 16 >> 2] | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { + break label$1 + } + break label$2; + case 1: + break label$3; + default: + break label$1; + }; + } + if (FLAC__subframe_add_verbatim($3 + 4 | 0, $1, $2, HEAP32[$3 + 288 >> 2], $4)) { + break label$1 + } + } + HEAP32[HEAP32[$0 >> 2] >> 2] = 7; + $5 = 0; + } + return $5; + } + + function FLAC__stream_encoder_set_ogg_serial_number($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 632 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_verify($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 4 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_channels($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 24 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_bits_per_sample($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 28 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_sample_rate($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 32 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_compression_level($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + var $2 = 0, $3 = 0, $4 = 0; + $3 = HEAP32[$0 >> 2]; + if (HEAP32[$3 >> 2] == 1) { + $2 = Math_imul($1 >>> 0 < 8 ? $1 : 8, 44); + $1 = $2 + 11184 | 0; + $4 = HEAP32[$1 + 4 >> 2]; + HEAP32[$3 + 16 >> 2] = HEAP32[$1 >> 2]; + HEAP32[$3 + 20 >> 2] = $4; + $3 = FLAC__stream_encoder_set_apodization($0, HEAP32[$1 + 40 >> 2]); + $1 = 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + $1 = $2 + 11184 | 0; + $2 = HEAP32[$1 + 32 >> 2]; + HEAP32[$0 + 576 >> 2] = HEAP32[$1 + 28 >> 2]; + HEAP32[$0 + 580 >> 2] = $2; + HEAP32[$0 + 568 >> 2] = HEAP32[$1 + 24 >> 2]; + HEAP32[$0 + 564 >> 2] = HEAP32[$1 + 16 >> 2]; + $2 = HEAP32[$1 + 12 >> 2]; + HEAP32[$0 + 556 >> 2] = HEAP32[$1 + 8 >> 2]; + HEAP32[$0 + 560 >> 2] = $2; + $1 = $3 & 1; + $0 = 1; + } else { + $0 = 0 + } + $0 = $0 & $1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_blocksize($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + HEAP32[$0 + 36 >> 2] = $1; + $0 = 1; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; + $0 = HEAP32[$0 >> 2]; + if (HEAP32[$0 >> 2] == 1) { + $6 = $2; + $7 = $0; + $8 = $1; + $4 = HEAP32[1363]; + $3 = $4 & 31; + if (32 <= ($4 & 63) >>> 0) { + $4 = -1 << $3; + $3 = 0; + } else { + $4 = (1 << $3) - 1 & -1 >>> 32 - $3 | -1 << $3; + $3 = -1 << $3; + } + $5 = $3 ^ -1; + $3 = $4 ^ -1; + $1 = ($2 | 0) == ($3 | 0) & $5 >>> 0 > $1 >>> 0 | $3 >>> 0 > $2 >>> 0; + HEAP32[$7 + 592 >> 2] = $1 ? $8 : $5; + HEAP32[$0 + 596 >> 2] = $1 ? $6 : $3; + $0 = 1; + } else { + $0 = 0 + } + return $0; + } + + function FLAC__stream_encoder_set_metadata($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0; + $3 = HEAP32[$0 >> 2]; + if (HEAP32[$3 >> 2] == 1) { + $4 = HEAP32[$3 + 600 >> 2]; + if ($4) { + dlfree($4); + $3 = HEAP32[$0 >> 2]; + HEAP32[$3 + 600 >> 2] = 0; + HEAP32[$3 + 604 >> 2] = 0; + } + $2 = $1 ? $2 : 0; + if ($2) { + $3 = safe_malloc_mul_2op_p(4, $2); + if (!$3) { + return 0 + } + $1 = memcpy($3, $1, $2 << 2); + $3 = HEAP32[$0 >> 2]; + HEAP32[$3 + 604 >> 2] = $2; + HEAP32[$3 + 600 >> 2] = $1; + } + $0 = $3 + 632 | 0; + if ($2 >>> HEAP32[1886]) { + $0 = 0 + } else { + HEAP32[$0 + 4 >> 2] = $2; + $0 = 1; + } + $0 = ($0 | 0) != 0; + } else { + $0 = 0 + } + return $0 | 0; + } + + function FLAC__stream_encoder_get_verify_decoder_state($0) { + $0 = $0 | 0; + if (!HEAP32[HEAP32[$0 >> 2] + 4 >> 2]) { + return 9 + } + return FLAC__stream_decoder_get_state(HEAP32[HEAP32[$0 + 4 >> 2] + 11752 >> 2]) | 0; + } + + function FLAC__stream_encoder_get_verify($0) { + $0 = $0 | 0; + return HEAP32[HEAP32[$0 >> 2] + 4 >> 2]; + } + + function FLAC__stream_encoder_process($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0; + $5 = HEAP32[$0 >> 2]; + $11 = HEAP32[$5 + 36 >> 2]; + $16 = $11 + 1 | 0; + $4 = HEAP32[$0 + 4 >> 2]; + $10 = HEAP32[$5 + 24 >> 2]; + $13 = $11 << 2; + label$1 : { + while (1) { + $3 = $16 - HEAP32[$4 + 7052 >> 2] | 0; + $6 = $2 - $7 | 0; + $6 = $3 >>> 0 < $6 >>> 0 ? $3 : $6; + if (HEAP32[$5 + 4 >> 2]) { + if ($10) { + $5 = $6 << 2; + $3 = 0; + while (1) { + $8 = $3 << 2; + memcpy(HEAP32[($8 + $4 | 0) + 11764 >> 2] + (HEAP32[$4 + 11800 >> 2] << 2) | 0, HEAP32[$1 + $8 >> 2] + ($7 << 2) | 0, $5); + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + $4 = $4 + 11800 | 0; + HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + $6; + } + if ($10) { + $5 = $6 << 2; + $4 = 0; + $3 = 0; + while (1) { + $8 = $3 << 2; + $12 = HEAP32[$8 + $1 >> 2]; + if (!$12) { + break label$1 + } + $9 = $8; + $8 = HEAP32[$0 + 4 >> 2]; + memcpy(HEAP32[($9 + $8 | 0) + 4 >> 2] + (HEAP32[$8 + 7052 >> 2] << 2) | 0, $12 + ($7 << 2) | 0, $5); + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + $5 = HEAP32[$0 >> 2]; + label$8 : { + if (HEAP32[$5 + 16 >> 2]) { + $4 = HEAP32[$0 + 4 >> 2]; + if ($7 >>> 0 >= $2 >>> 0) { + break label$8 + } + $3 = HEAP32[$4 + 7052 >> 2]; + if ($3 >>> 0 > $11 >>> 0) { + break label$8 + } + $8 = HEAP32[$4 + 40 >> 2]; + $12 = HEAP32[$4 + 36 >> 2]; + $17 = HEAP32[$1 + 4 >> 2]; + $18 = HEAP32[$1 >> 2]; + while (1) { + $14 = $3 << 2; + $9 = $7 << 2; + $15 = $9 + $18 | 0; + $9 = $9 + $17 | 0; + HEAP32[$14 + $8 >> 2] = HEAP32[$15 >> 2] - HEAP32[$9 >> 2]; + HEAP32[$12 + $14 >> 2] = HEAP32[$9 >> 2] + HEAP32[$15 >> 2] >> 1; + $7 = $7 + 1 | 0; + if ($7 >>> 0 >= $2 >>> 0) { + break label$8 + } + $3 = $3 + 1 | 0; + if ($3 >>> 0 <= $11 >>> 0) { + continue + } + break; + }; + break label$8; + } + $7 = $7 + $6 | 0; + $4 = HEAP32[$0 + 4 >> 2]; + } + $3 = HEAP32[$4 + 7052 >> 2] + $6 | 0; + HEAP32[$4 + 7052 >> 2] = $3; + if ($3 >>> 0 > $11 >>> 0) { + $4 = 0; + if (!process_frame_($0, 0, 0)) { + break label$1 + } + if ($10) { + $4 = HEAP32[$0 + 4 >> 2]; + $3 = 0; + while (1) { + $6 = HEAP32[($4 + ($3 << 2) | 0) + 4 >> 2]; + HEAP32[$6 >> 2] = HEAP32[$6 + $13 >> 2]; + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + $4 = HEAP32[$0 + 4 >> 2]; + $5 = HEAP32[$0 >> 2]; + if (HEAP32[$5 + 16 >> 2]) { + $3 = HEAP32[$4 + 36 >> 2]; + HEAP32[$3 >> 2] = HEAP32[$3 + $13 >> 2]; + $3 = HEAP32[$4 + 40 >> 2]; + HEAP32[$3 >> 2] = HEAP32[$3 + $13 >> 2]; + } + HEAP32[$4 + 7052 >> 2] = 1; + } + if ($7 >>> 0 < $2 >>> 0) { + continue + } + break; + }; + $4 = 1; + } + return $4 | 0; + } + + function FLAC__stream_encoder_process_interleaved($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; + $3 = HEAP32[$0 >> 2]; + $9 = HEAP32[$3 + 36 >> 2]; + $16 = $9 + 1 | 0; + label$1 : { + label$2 : { + $10 = HEAP32[$3 + 24 >> 2]; + if (!(!HEAP32[$3 + 16 >> 2] | ($10 | 0) != 2)) { + while (1) { + $4 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$3 + 4 >> 2]) { + $3 = HEAP32[$4 + 11800 >> 2]; + $5 = $16 - HEAP32[$4 + 7052 >> 2] | 0; + $6 = $2 - $7 | 0; + $8 = $5 >>> 0 < $6 >>> 0 ? $5 : $6; + label$6 : { + if (!$8) { + break label$6 + } + if (!$10) { + $3 = $3 + $8 | 0; + break label$6; + } + $5 = $7 << 1; + $11 = HEAP32[$4 + 11768 >> 2]; + $15 = HEAP32[$4 + 11764 >> 2]; + $6 = 0; + while (1) { + $13 = $3 << 2; + $14 = $5 << 2; + HEAP32[$13 + $15 >> 2] = HEAP32[$14 + $1 >> 2]; + HEAP32[$11 + $13 >> 2] = HEAP32[($14 | 4) + $1 >> 2]; + $3 = $3 + 1 | 0; + $5 = $5 + 2 | 0; + $6 = $6 + 1 | 0; + if (($8 | 0) != ($6 | 0)) { + continue + } + break; + }; + } + HEAP32[$4 + 11800 >> 2] = $3; + } + $5 = $7 >>> 0 < $2 >>> 0; + $3 = HEAP32[$4 + 7052 >> 2]; + label$9 : { + if ($3 >>> 0 > $9 >>> 0 | $7 >>> 0 >= $2 >>> 0) { + break label$9 + } + $11 = HEAP32[$4 + 40 >> 2]; + $15 = HEAP32[$4 + 8 >> 2]; + $13 = HEAP32[$4 + 36 >> 2]; + $14 = HEAP32[$4 + 4 >> 2]; + while (1) { + $5 = $3 << 2; + $8 = ($12 << 2) + $1 | 0; + $6 = HEAP32[$8 >> 2]; + HEAP32[$5 + $14 >> 2] = $6; + $8 = HEAP32[$8 + 4 >> 2]; + HEAP32[$5 + $15 >> 2] = $8; + HEAP32[$5 + $11 >> 2] = $6 - $8; + HEAP32[$5 + $13 >> 2] = $6 + $8 >> 1; + $3 = $3 + 1 | 0; + $12 = $12 + 2 | 0; + $7 = $7 + 1 | 0; + $5 = $7 >>> 0 < $2 >>> 0; + if ($7 >>> 0 >= $2 >>> 0) { + break label$9 + } + if ($3 >>> 0 <= $9 >>> 0) { + continue + } + break; + }; + } + HEAP32[$4 + 7052 >> 2] = $3; + if ($3 >>> 0 > $9 >>> 0) { + $3 = 0; + if (!process_frame_($0, 0, 0)) { + break label$1 + } + $3 = HEAP32[$0 + 4 >> 2]; + $6 = HEAP32[$3 + 4 >> 2]; + $4 = $6; + $6 = $9 << 2; + HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; + $4 = HEAP32[$3 + 8 >> 2]; + HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; + $4 = HEAP32[$3 + 36 >> 2]; + HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; + $4 = HEAP32[$3 + 40 >> 2]; + HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; + HEAP32[$3 + 7052 >> 2] = 1; + } + if (!$5) { + break label$2 + } + $3 = HEAP32[$0 >> 2]; + continue; + } + } + while (1) { + $7 = HEAP32[$0 + 4 >> 2]; + if (HEAP32[$3 + 4 >> 2]) { + $6 = HEAP32[$7 + 11800 >> 2]; + $3 = $16 - HEAP32[$7 + 7052 >> 2] | 0; + $5 = $2 - $4 | 0; + $8 = $3 >>> 0 < $5 >>> 0 ? $3 : $5; + label$14 : { + if (!$8) { + break label$14 + } + if (!$10) { + $6 = $6 + $8 | 0; + break label$14; + } + $5 = Math_imul($4, $10); + $11 = 0; + while (1) { + $3 = 0; + while (1) { + HEAP32[HEAP32[($7 + ($3 << 2) | 0) + 11764 >> 2] + ($6 << 2) >> 2] = HEAP32[($5 << 2) + $1 >> 2]; + $5 = $5 + 1 | 0; + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + $6 = $6 + 1 | 0; + $11 = $11 + 1 | 0; + if (($8 | 0) != ($11 | 0)) { + continue + } + break; + }; + } + HEAP32[$7 + 11800 >> 2] = $6; + } + $6 = $4 >>> 0 < $2 >>> 0; + $5 = HEAP32[$7 + 7052 >> 2]; + label$18 : { + if ($5 >>> 0 > $9 >>> 0 | $4 >>> 0 >= $2 >>> 0) { + break label$18 + } + if ($10) { + while (1) { + $3 = 0; + while (1) { + HEAP32[HEAP32[($7 + ($3 << 2) | 0) + 4 >> 2] + ($5 << 2) >> 2] = HEAP32[($12 << 2) + $1 >> 2]; + $12 = $12 + 1 | 0; + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + $5 = $5 + 1 | 0; + $4 = $4 + 1 | 0; + $6 = $4 >>> 0 < $2 >>> 0; + if ($4 >>> 0 >= $2 >>> 0) { + break label$18 + } + if ($5 >>> 0 <= $9 >>> 0) { + continue + } + break label$18; + } + } + while (1) { + $5 = $5 + 1 | 0; + $4 = $4 + 1 | 0; + $6 = $4 >>> 0 < $2 >>> 0; + if ($4 >>> 0 >= $2 >>> 0) { + break label$18 + } + if ($5 >>> 0 <= $9 >>> 0) { + continue + } + break; + }; + } + HEAP32[$7 + 7052 >> 2] = $5; + if ($5 >>> 0 > $9 >>> 0) { + $3 = 0; + if (!process_frame_($0, 0, 0)) { + break label$1 + } + $5 = HEAP32[$0 + 4 >> 2]; + if ($10) { + $3 = 0; + while (1) { + $7 = HEAP32[($5 + ($3 << 2) | 0) + 4 >> 2]; + HEAP32[$7 >> 2] = HEAP32[$7 + ($9 << 2) >> 2]; + $3 = $3 + 1 | 0; + if (($10 | 0) != ($3 | 0)) { + continue + } + break; + }; + } + HEAP32[$5 + 7052 >> 2] = 1; + } + if (!$6) { + break label$2 + } + $3 = HEAP32[$0 >> 2]; + continue; + }; + } + $3 = 1; + } + return $3 | 0; + } + + function find_best_partition_order_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) { + var $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0; + $26 = $4 + $5 | 0; + $14 = FLAC__format_get_max_rice_partition_order_from_blocksize_limited_max_and_predictor_order($9, $26, $5); + $22 = $14 >>> 0 > $8 >>> 0 ? $8 : $14; + FUNCTION_TABLE[HEAP32[$0 + 7220 >> 2]]($1, $2, $4, $5, $22, $14, $10); + label$1 : { + if (!$11) { + break label$1 + } + $10 = 0; + $8 = 0; + if (($14 | 0) >= 0) { + $8 = 1 << $14; + $20 = $8 >>> 0 > 1 ? $8 : 1; + $16 = $26 >>> $14 | 0; + while (1) { + $17 = 0; + $9 = $13; + $18 = 0; + $27 = ($15 << 2) + $3 | 0; + label$4 : { + label$5 : { + $23 = $15 ? 0 : $5; + $19 = $16 - $23 | 0; + if (!$19) { + break label$5 + } + while (1) { + $21 = $17; + $17 = HEAP32[($9 << 2) + $1 >> 2]; + $17 = $21 | $17 >> 31 ^ $17; + $9 = $9 + 1 | 0; + $18 = $18 + 1 | 0; + if (($19 | 0) != ($18 | 0)) { + continue + } + break; + }; + $13 = ($13 + $16 | 0) - $23 | 0; + if (!$17) { + break label$5 + } + $9 = (Math_clz32($17) ^ 31) + 2 | 0; + break label$4; + } + $9 = 1; + } + HEAP32[$27 >> 2] = $9; + $15 = $15 + 1 | 0; + if (($20 | 0) != ($15 | 0)) { + continue + } + break; + }; + } + if (($14 | 0) <= ($22 | 0)) { + break label$1 + } + $1 = $14; + while (1) { + $1 = $1 + -1 | 0; + $9 = 0; + while (1) { + $13 = ($10 << 2) + $3 | 0; + $15 = HEAP32[$13 >> 2]; + $13 = HEAP32[$13 + 4 >> 2]; + HEAP32[($8 << 2) + $3 >> 2] = $15 >>> 0 > $13 >>> 0 ? $15 : $13; + $8 = $8 + 1 | 0; + $10 = $10 + 2 | 0; + $9 = $9 + 1 | 0; + if (!($9 >>> $1)) { + continue + } + break; + }; + if (($1 | 0) > ($22 | 0)) { + continue + } + break; + }; + } + label$9 : { + if (($14 | 0) < ($22 | 0)) { + HEAP32[$12 + 4 >> 2] = 0; + $2 = 6; + break label$9; + } + $28 = HEAP32[1407]; + $40 = $28 + (Math_imul($6 + 1 | 0, $4) - ($4 >>> 1 | 0) | 0) | 0; + $35 = $7 + -1 | 0; + $36 = HEAP32[1409] + HEAP32[1408] | 0; + $23 = HEAP32[1406] + HEAP32[1405] | 0; + $27 = $6 + -1 | 0; + while (1) { + label$12 : { + $20 = $14; + $37 = !$29; + $1 = Math_imul($37, 12) + $0 | 0; + $8 = $1 + 11724 | 0; + FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($8, $14 >>> 0 > 6 ? $14 : 6); + $38 = ($30 << 2) + $3 | 0; + $25 = ($30 << 3) + $2 | 0; + $39 = HEAP32[$1 + 11728 >> 2]; + $31 = HEAP32[$8 >> 2]; + label$13 : { + if ($14) { + $32 = $26 >>> $20 | 0; + if ($32 >>> 0 <= $5 >>> 0) { + break label$12 + } + $18 = 0; + $33 = 0; + $21 = $23; + if (!$11) { + while (1) { + $17 = $32 - ($18 ? 0 : $5) | 0; + $1 = $25 + ($18 << 3) | 0; + $13 = HEAP32[$1 + 4 >> 2]; + $16 = HEAP32[$1 >> 2]; + label$17 : { + if (!$13 & $16 >>> 0 >= 268435457 | $13 >>> 0 > 0) { + $1 = $17; + $10 = 0; + $8 = 0; + label$19 : { + if (($13 | 0) == 16777216 & $16 >>> 0 > 0 | $13 >>> 0 > 16777216) { + $14 = $1; + $9 = 0; + break label$19; + } + $14 = $1; + $9 = 0; + $15 = $1 >>> 25 | 0; + $19 = $1 << 7; + if (($13 | 0) == ($15 | 0) & $19 >>> 0 >= $16 >>> 0 | $15 >>> 0 > $13 >>> 0) { + break label$19 + } + while (1) { + $8 = $8 + 8 | 0; + $15 = $10 << 15 | $1 >>> 17; + $19 = $1 << 15; + $9 = $10 << 8 | $1 >>> 24; + $14 = $1 << 8; + $1 = $14; + $10 = $9; + if (($13 | 0) == ($15 | 0) & $19 >>> 0 < $16 >>> 0 | $15 >>> 0 < $13 >>> 0) { + continue + } + break; + }; + } + if (($9 | 0) == ($13 | 0) & $14 >>> 0 >= $16 >>> 0 | $9 >>> 0 > $13 >>> 0) { + break label$17 + } + while (1) { + $8 = $8 + 1 | 0; + $1 = $14; + $15 = $9 << 1 | $1 >>> 31; + $14 = $1 << 1; + $1 = $14; + $9 = $15; + if (($13 | 0) == ($9 | 0) & $1 >>> 0 < $16 >>> 0 | $9 >>> 0 < $13 >>> 0) { + continue + } + break; + }; + break label$17; + } + $8 = 0; + $10 = $17; + $1 = $16; + if ($10 << 3 >>> 0 < $1 >>> 0) { + while (1) { + $8 = $8 + 4 | 0; + $9 = $10 << 7; + $10 = $10 << 4; + if ($9 >>> 0 < $1 >>> 0) { + continue + } + break; + } + } + if ($10 >>> 0 >= $1 >>> 0) { + break label$17 + } + while (1) { + $8 = $8 + 1 | 0; + $10 = $10 << 1; + if ($10 >>> 0 < $1 >>> 0) { + continue + } + break; + }; + } + $8 = $8 >>> 0 < $7 >>> 0 ? $8 : $35; + $10 = $8 + -1 | 0; + $1 = $10 & 31; + $1 = (($28 - ($17 >>> 1 | 0) | 0) + Math_imul($17, $8 + 1 | 0) | 0) + ($8 ? (32 <= ($10 & 63) >>> 0 ? $13 >>> $1 | 0 : ((1 << $1) - 1 & $13) << 32 - $1 | $16 >>> $1) : $16 << 1) | 0; + $33 = ($1 | 0) == -1 ? $33 : $8; + HEAP32[$31 + ($18 << 2) >> 2] = $33; + $21 = $1 + $21 | 0; + $18 = $18 + 1 | 0; + if (!($18 >>> $20)) { + continue + } + break label$13; + } + } + while (1) { + $17 = $32 - ($18 ? 0 : $5) | 0; + $1 = $25 + ($18 << 3) | 0; + $13 = HEAP32[$1 + 4 >> 2]; + $16 = HEAP32[$1 >> 2]; + label$27 : { + label$28 : { + if (!$13 & $16 >>> 0 >= 268435457 | $13 >>> 0 > 0) { + $1 = $17; + $10 = 0; + $8 = 0; + if (($13 | 0) == 16777216 & $16 >>> 0 > 0 | $13 >>> 0 > 16777216) { + break label$28 + } + $14 = $1; + $9 = 0; + $15 = $1 >>> 25 | 0; + $19 = $1 << 7; + if (($13 | 0) == ($15 | 0) & $19 >>> 0 >= $16 >>> 0 | $15 >>> 0 > $13 >>> 0) { + break label$28 + } + while (1) { + $8 = $8 + 8 | 0; + $1 = $9; + $10 = $14; + $15 = $1 << 15 | $10 >>> 17; + $19 = $10 << 15; + $9 = $1 << 8; + $1 = $10; + $9 = $9 | $1 >>> 24; + $1 = $1 << 8; + $14 = $1; + $10 = $9; + if (($13 | 0) == ($15 | 0) & $19 >>> 0 < $16 >>> 0 | $15 >>> 0 < $13 >>> 0) { + continue + } + break; + }; + break label$28; + } + $8 = 0; + $10 = $17; + $1 = $16; + if ($10 << 3 >>> 0 < $1 >>> 0) { + while (1) { + $8 = $8 + 4 | 0; + $9 = $10 << 7; + $10 = $10 << 4; + if ($9 >>> 0 < $1 >>> 0) { + continue + } + break; + } + } + if ($10 >>> 0 >= $1 >>> 0) { + break label$27 + } + while (1) { + $8 = $8 + 1 | 0; + $10 = $10 << 1; + if ($10 >>> 0 < $1 >>> 0) { + continue + } + break; + }; + break label$27; + } + if (($10 | 0) == ($13 | 0) & $1 >>> 0 >= $16 >>> 0 | $10 >>> 0 > $13 >>> 0) { + break label$27 + } + while (1) { + $8 = $8 + 1 | 0; + $15 = $10 << 1 | $1 >>> 31; + $1 = $1 << 1; + $10 = $15; + if (($13 | 0) == ($10 | 0) & $1 >>> 0 < $16 >>> 0 | $10 >>> 0 < $13 >>> 0) { + continue + } + break; + }; + } + $9 = $18 << 2; + $1 = HEAP32[$9 + $38 >> 2]; + $19 = $1; + $10 = Math_imul($1, $17) + $36 | 0; + $8 = $8 >>> 0 < $7 >>> 0 ? $8 : $35; + $15 = $8 + -1 | 0; + $1 = $15 & 31; + $14 = (($28 - ($17 >>> 1 | 0) | 0) + Math_imul($17, $8 + 1 | 0) | 0) + ($8 ? (32 <= ($15 & 63) >>> 0 ? $13 >>> $1 | 0 : ((1 << $1) - 1 & $13) << 32 - $1 | $16 >>> $1) : $16 << 1) | 0; + $1 = $10 >>> 0 > $14 >>> 0; + HEAP32[$9 + $39 >> 2] = $1 ? 0 : $19; + HEAP32[$9 + $31 >> 2] = $1 ? $8 : 0; + $21 = ($1 ? $14 : $10) + $21 | 0; + $18 = $18 + 1 | 0; + if (!($18 >>> $20)) { + continue + } + break; + }; + break label$13; + } + $9 = HEAP32[$25 + 4 >> 2]; + $1 = $27; + $8 = $1 & 31; + $10 = HEAP32[$25 >> 2]; + $8 = ($6 ? (32 <= ($1 & 63) >>> 0 ? $9 >>> $8 | 0 : ((1 << $8) - 1 & $9) << 32 - $8 | $10 >>> $8) : $10 << 1) + $40 | 0; + $10 = ($8 | 0) == -1 ? 0 : $6; + if ($11) { + $9 = HEAP32[$38 >> 2]; + $14 = Math_imul($9, $4) + $36 | 0; + $1 = $14 >>> 0 > $8 >>> 0; + HEAP32[$39 >> 2] = $1 ? 0 : $9; + $10 = $1 ? $10 : 0; + $8 = $1 ? $8 : $14; + } + HEAP32[$31 >> 2] = $10; + $21 = $8 + $23 | 0; + } + $1 = $34 + -1 >>> 0 < $21 >>> 0; + $24 = $1 ? $24 : $20; + $29 = $1 ? $29 : $37; + $34 = $1 ? $34 : $21; + $14 = $20 + -1 | 0; + $30 = (1 << $20) + $30 | 0; + if (($20 | 0) > ($22 | 0)) { + continue + } + } + break; + }; + HEAP32[$12 + 4 >> 2] = $24; + $2 = $24 >>> 0 > 6 ? $24 : 6; + } + $1 = HEAP32[$12 + 8 >> 2]; + FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($1, $2); + $2 = Math_imul($29, 12) + $0 | 0; + $0 = 1 << $24; + $3 = $0 << 2; + memcpy(HEAP32[$1 >> 2], HEAP32[$2 + 11724 >> 2], $3); + if ($11) { + memcpy(HEAP32[$1 + 4 >> 2], HEAP32[$2 + 11728 >> 2], $3) + } + $0 = $0 >>> 0 > 1 ? $0 : 1; + $2 = HEAP32[1410]; + $1 = HEAP32[$1 >> 2]; + $8 = 0; + label$37 : { + while (1) { + if (HEAPU32[$1 + ($8 << 2) >> 2] < $2 >>> 0) { + $8 = $8 + 1 | 0; + if (($0 | 0) != ($8 | 0)) { + continue + } + break label$37; + } + break; + }; + HEAP32[$12 >> 2] = 1; + } + return $34; + } + + function stackSave() { + return global$0 | 0; + } + + function stackRestore($0) { + $0 = $0 | 0; + global$0 = $0; + } + + function stackAlloc($0) { + $0 = $0 | 0; + $0 = global$0 - $0 & -16; + global$0 = $0; + return $0 | 0; + } + + function __growWasmMemory($0) { + $0 = $0 | 0; + return __wasm_memory_grow($0 | 0) | 0; + } + + function dynCall_iii($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + return FUNCTION_TABLE[$0]($1, $2) | 0; + } + + function dynCall_ii($0, $1) { + $0 = $0 | 0; + $1 = $1 | 0; + return FUNCTION_TABLE[$0]($1) | 0; + } + + function dynCall_iiii($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + return FUNCTION_TABLE[$0]($1, $2, $3) | 0; + } + + function dynCall_viiiiii($0, $1, $2, $3, $4, $5, $6) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + FUNCTION_TABLE[$0]($1, $2, $3, $4, $5, $6); + } + + function dynCall_iiiii($0, $1, $2, $3, $4) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + return FUNCTION_TABLE[$0]($1, $2, $3, $4) | 0; + } + + function dynCall_viiiiiii($0, $1, $2, $3, $4, $5, $6, $7) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $5 = $5 | 0; + $6 = $6 | 0; + $7 = $7 | 0; + FUNCTION_TABLE[$0]($1, $2, $3, $4, $5, $6, $7); + } + + function dynCall_viiii($0, $1, $2, $3, $4) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + FUNCTION_TABLE[$0]($1, $2, $3, $4); + } + + function dynCall_viii($0, $1, $2, $3) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + FUNCTION_TABLE[$0]($1, $2, $3); + } + + function legalstub$FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + return FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) | 0; + } + + function legalstub$dynCall_jiji($0, $1, $2, $3, $4) { + $0 = $0 | 0; + $1 = $1 | 0; + $2 = $2 | 0; + $3 = $3 | 0; + $4 = $4 | 0; + $0 = FUNCTION_TABLE[$0]($1, $2, $3, $4) | 0; + setTempRet0(i64toi32_i32$HIGH_BITS | 0); + return $0 | 0; + } + + function _ZN17compiler_builtins3int3mul3Mul3mul17h070e9a1c69faec5bE($0, $1, $2, $3) { + var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; + $4 = $2 >>> 16 | 0; + $5 = $0 >>> 16 | 0; + $9 = Math_imul($4, $5); + $6 = $2 & 65535; + $7 = $0 & 65535; + $8 = Math_imul($6, $7); + $5 = ($8 >>> 16 | 0) + Math_imul($5, $6) | 0; + $4 = ($5 & 65535) + Math_imul($4, $7) | 0; + $0 = (Math_imul($1, $2) + $9 | 0) + Math_imul($0, $3) + ($5 >>> 16) + ($4 >>> 16) | 0; + $1 = $8 & 65535 | $4 << 16; + i64toi32_i32$HIGH_BITS = $0; + return $1; + } + + function _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, $2) { + var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0; + label$1 : { + label$2 : { + label$3 : { + label$4 : { + label$5 : { + label$6 : { + label$7 : { + label$9 : { + label$11 : { + $3 = $1; + if ($3) { + $4 = $2; + if (!$4) { + break label$11 + } + break label$9; + } + $1 = $0; + $0 = ($0 >>> 0) / ($2 >>> 0) | 0; + __wasm_intrinsics_temp_i64 = $1 - Math_imul($0, $2) | 0; + __wasm_intrinsics_temp_i64$hi = 0; + i64toi32_i32$HIGH_BITS = 0; + return $0; + } + if (!$0) { + break label$7 + } + break label$6; + } + $6 = $4 + -1 | 0; + if (!($6 & $4)) { + break label$5 + } + $6 = (Math_clz32($4) + 33 | 0) - Math_clz32($3) | 0; + $7 = 0 - $6 | 0; + break label$3; + } + __wasm_intrinsics_temp_i64 = 0; + $0 = ($3 >>> 0) / 0 | 0; + __wasm_intrinsics_temp_i64$hi = $3 - Math_imul($0, 0) | 0; + i64toi32_i32$HIGH_BITS = 0; + return $0; + } + $3 = 32 - Math_clz32($3) | 0; + if ($3 >>> 0 < 31) { + break label$4 + } + break label$2; + } + __wasm_intrinsics_temp_i64 = $0 & $6; + __wasm_intrinsics_temp_i64$hi = 0; + if (($4 | 0) == 1) { + break label$1 + } + $3 = __wasm_ctz_i32($4); + $2 = $3 & 31; + if (32 <= ($3 & 63) >>> 0) { + $4 = 0; + $0 = $1 >>> $2 | 0; + } else { + $4 = $1 >>> $2 | 0; + $0 = ((1 << $2) - 1 & $1) << 32 - $2 | $0 >>> $2; + } + i64toi32_i32$HIGH_BITS = $4; + return $0; + } + $6 = $3 + 1 | 0; + $7 = 63 - $3 | 0; + } + $3 = $1; + $4 = $6 & 63; + $5 = $4 & 31; + if (32 <= $4 >>> 0) { + $4 = 0; + $5 = $3 >>> $5 | 0; + } else { + $4 = $3 >>> $5 | 0; + $5 = ((1 << $5) - 1 & $3) << 32 - $5 | $0 >>> $5; + } + $7 = $7 & 63; + $3 = $7 & 31; + if (32 <= $7 >>> 0) { + $1 = $0 << $3; + $0 = 0; + } else { + $1 = (1 << $3) - 1 & $0 >>> 32 - $3 | $1 << $3; + $0 = $0 << $3; + } + if ($6) { + $7 = -1; + $3 = $2 + -1 | 0; + if (($3 | 0) != -1) { + $7 = 0 + } + while (1) { + $8 = $5 << 1 | $1 >>> 31; + $9 = $8; + $4 = $4 << 1 | $5 >>> 31; + $8 = $7 - ($4 + ($3 >>> 0 < $8 >>> 0) | 0) >> 31; + $10 = $2 & $8; + $5 = $9 - $10 | 0; + $4 = $4 - ($9 >>> 0 < $10 >>> 0) | 0; + $1 = $1 << 1 | $0 >>> 31; + $0 = $11 | $0 << 1; + $8 = $8 & 1; + $11 = $8; + $6 = $6 + -1 | 0; + if ($6) { + continue + } + break; + }; + } + __wasm_intrinsics_temp_i64 = $5; + __wasm_intrinsics_temp_i64$hi = $4; + i64toi32_i32$HIGH_BITS = $1 << 1 | $0 >>> 31; + return $8 | $0 << 1; + } + __wasm_intrinsics_temp_i64 = $0; + __wasm_intrinsics_temp_i64$hi = $1; + $0 = 0; + $1 = 0; + } + i64toi32_i32$HIGH_BITS = $1; + return $0; + } + + function __wasm_ctz_i32($0) { + if ($0) { + return 31 - Math_clz32($0 + -1 ^ $0) | 0 + } + return 32; + } + + function __wasm_i64_mul($0, $1, $2, $3) { + $0 = _ZN17compiler_builtins3int3mul3Mul3mul17h070e9a1c69faec5bE($0, $1, $2, $3); + return $0; + } + + function __wasm_i64_udiv($0, $1, $2) { + return _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, $2); + } + + function __wasm_i64_urem($0, $1) { + _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, 588); + i64toi32_i32$HIGH_BITS = __wasm_intrinsics_temp_i64$hi; + return __wasm_intrinsics_temp_i64; + } + + function __wasm_rotl_i32($0, $1) { + var $2 = 0, $3 = 0; + $2 = $1 & 31; + $3 = (-1 >>> $2 & $0) << $2; + $2 = $0; + $0 = 0 - $1 & 31; + return $3 | ($2 & -1 << $0) >>> $0; + } + + // EMSCRIPTEN_END_FUNCS +; + FUNCTION_TABLE[1] = seekpoint_compare_; + FUNCTION_TABLE[2] = __stdio_close; + FUNCTION_TABLE[3] = __stdio_read; + FUNCTION_TABLE[4] = __stdio_seek; + FUNCTION_TABLE[5] = FLAC__lpc_restore_signal; + FUNCTION_TABLE[6] = FLAC__lpc_restore_signal_wide; + FUNCTION_TABLE[7] = read_callback_; + FUNCTION_TABLE[8] = read_callback_proxy_; + FUNCTION_TABLE[9] = __emscripten_stdout_close; + FUNCTION_TABLE[10] = __stdio_write; + FUNCTION_TABLE[11] = __emscripten_stdout_seek; + FUNCTION_TABLE[12] = FLAC__lpc_compute_residual_from_qlp_coefficients; + FUNCTION_TABLE[13] = FLAC__lpc_compute_residual_from_qlp_coefficients_wide; + FUNCTION_TABLE[14] = FLAC__fixed_compute_best_predictor_wide; + FUNCTION_TABLE[15] = FLAC__fixed_compute_best_predictor; + FUNCTION_TABLE[16] = precompute_partition_info_sums_; + FUNCTION_TABLE[17] = FLAC__lpc_compute_autocorrelation; + FUNCTION_TABLE[18] = verify_read_callback_; + FUNCTION_TABLE[19] = verify_write_callback_; + FUNCTION_TABLE[20] = verify_metadata_callback_; + FUNCTION_TABLE[21] = verify_error_callback_; + function __wasm_memory_size() { + return buffer.byteLength / 65536 | 0; + } + + function __wasm_memory_grow(pagesToAdd) { + pagesToAdd = pagesToAdd | 0; + var oldPages = __wasm_memory_size() | 0; + var newPages = oldPages + pagesToAdd | 0; + if ((oldPages < newPages) && (newPages < 65536)) { + var newBuffer = new ArrayBuffer(Math_imul(newPages, 65536)); + var newHEAP8 = new global.Int8Array(newBuffer); + newHEAP8.set(HEAP8); + HEAP8 = newHEAP8; + HEAP8 = new global.Int8Array(newBuffer); + HEAP16 = new global.Int16Array(newBuffer); + HEAP32 = new global.Int32Array(newBuffer); + HEAPU8 = new global.Uint8Array(newBuffer); + HEAPU16 = new global.Uint16Array(newBuffer); + HEAPU32 = new global.Uint32Array(newBuffer); + HEAPF32 = new global.Float32Array(newBuffer); + HEAPF64 = new global.Float64Array(newBuffer); + buffer = newBuffer; + memory.buffer = newBuffer; + } + return oldPages; + } + + return { + "__wasm_call_ctors": __wasm_call_ctors, + "FLAC__stream_decoder_new": FLAC__stream_decoder_new, + "FLAC__stream_decoder_delete": FLAC__stream_decoder_delete, + "FLAC__stream_decoder_finish": FLAC__stream_decoder_finish, + "FLAC__stream_decoder_init_stream": FLAC__stream_decoder_init_stream, + "FLAC__stream_decoder_reset": FLAC__stream_decoder_reset, + "FLAC__stream_decoder_init_ogg_stream": FLAC__stream_decoder_init_ogg_stream, + "FLAC__stream_decoder_set_ogg_serial_number": FLAC__stream_decoder_set_ogg_serial_number, + "FLAC__stream_decoder_set_md5_checking": FLAC__stream_decoder_set_md5_checking, + "FLAC__stream_decoder_set_metadata_respond": FLAC__stream_decoder_set_metadata_respond, + "FLAC__stream_decoder_set_metadata_respond_application": FLAC__stream_decoder_set_metadata_respond_application, + "FLAC__stream_decoder_set_metadata_respond_all": FLAC__stream_decoder_set_metadata_respond_all, + "FLAC__stream_decoder_set_metadata_ignore": FLAC__stream_decoder_set_metadata_ignore, + "FLAC__stream_decoder_set_metadata_ignore_application": FLAC__stream_decoder_set_metadata_ignore_application, + "FLAC__stream_decoder_set_metadata_ignore_all": FLAC__stream_decoder_set_metadata_ignore_all, + "FLAC__stream_decoder_get_state": FLAC__stream_decoder_get_state, + "FLAC__stream_decoder_get_md5_checking": FLAC__stream_decoder_get_md5_checking, + "FLAC__stream_decoder_process_single": FLAC__stream_decoder_process_single, + "FLAC__stream_decoder_process_until_end_of_metadata": FLAC__stream_decoder_process_until_end_of_metadata, + "FLAC__stream_decoder_process_until_end_of_stream": FLAC__stream_decoder_process_until_end_of_stream, + "FLAC__stream_encoder_new": FLAC__stream_encoder_new, + "FLAC__stream_encoder_delete": FLAC__stream_encoder_delete, + "FLAC__stream_encoder_finish": FLAC__stream_encoder_finish, + "FLAC__stream_encoder_init_stream": FLAC__stream_encoder_init_stream, + "FLAC__stream_encoder_init_ogg_stream": FLAC__stream_encoder_init_ogg_stream, + "FLAC__stream_encoder_set_ogg_serial_number": FLAC__stream_encoder_set_ogg_serial_number, + "FLAC__stream_encoder_set_verify": FLAC__stream_encoder_set_verify, + "FLAC__stream_encoder_set_channels": FLAC__stream_encoder_set_channels, + "FLAC__stream_encoder_set_bits_per_sample": FLAC__stream_encoder_set_bits_per_sample, + "FLAC__stream_encoder_set_sample_rate": FLAC__stream_encoder_set_sample_rate, + "FLAC__stream_encoder_set_compression_level": FLAC__stream_encoder_set_compression_level, + "FLAC__stream_encoder_set_blocksize": FLAC__stream_encoder_set_blocksize, + "FLAC__stream_encoder_set_total_samples_estimate": legalstub$FLAC__stream_encoder_set_total_samples_estimate, + "FLAC__stream_encoder_set_metadata": FLAC__stream_encoder_set_metadata, + "FLAC__stream_encoder_get_state": FLAC__stream_decoder_get_state, + "FLAC__stream_encoder_get_verify_decoder_state": FLAC__stream_encoder_get_verify_decoder_state, + "FLAC__stream_encoder_get_verify": FLAC__stream_encoder_get_verify, + "FLAC__stream_encoder_process": FLAC__stream_encoder_process, + "FLAC__stream_encoder_process_interleaved": FLAC__stream_encoder_process_interleaved, + "__errno_location": __errno_location, + "stackSave": stackSave, + "stackRestore": stackRestore, + "stackAlloc": stackAlloc, + "malloc": dlmalloc, + "free": dlfree, + "__growWasmMemory": __growWasmMemory, + "dynCall_iii": dynCall_iii, + "dynCall_ii": dynCall_ii, + "dynCall_iiii": dynCall_iiii, + "dynCall_jiji": legalstub$dynCall_jiji, + "dynCall_viiiiii": dynCall_viiiiii, + "dynCall_iiiii": dynCall_iiiii, + "dynCall_viiiiiii": dynCall_viiiiiii, + "dynCall_viiii": dynCall_viiii, + "dynCall_viii": dynCall_viii + }; +} + +var bufferView = new Uint8Array(wasmMemory.buffer); +for (var base64ReverseLookup = new Uint8Array(123/*'z'+1*/), i = 25; i >= 0; --i) { + base64ReverseLookup[48+i] = 52+i; // '0-9' + base64ReverseLookup[65+i] = i; // 'A-Z' + base64ReverseLookup[97+i] = 26+i; // 'a-z' + } + base64ReverseLookup[43] = 62; // '+' + base64ReverseLookup[47] = 63; // '/' + /** @noinline Inlining this function would mean expanding the base64 string 4x times in the source code, which Closure seems to be happy to do. */ + function base64DecodeToExistingUint8Array(uint8Array, offset, b64) { + var b1, b2, i = 0, j = offset, bLength = b64.length, end = offset + (bLength*3>>2) - (b64[bLength-2] == '=') - (b64[bLength-1] == '='); + for (; i < bLength; i += 4) { + b1 = base64ReverseLookup[b64.charCodeAt(i+1)]; + b2 = base64ReverseLookup[b64.charCodeAt(i+2)]; + uint8Array[j++] = base64ReverseLookup[b64.charCodeAt(i)] << 2 | b1 >> 4; + if (j < end) uint8Array[j++] = b1 << 4 | b2 >> 2; + if (j < end) uint8Array[j++] = b2 << 6 | base64ReverseLookup[b64.charCodeAt(i+3)]; + } + } + base64DecodeToExistingUint8Array(bufferView, 1025, "Bw4JHBsSFTg/NjEkIyotcHd+eWxrYmVIT0ZBVFNaXeDn7un8+/L12N/W0cTDys2Ql56ZjIuChaivpqG0s7q9x8DJztvc1dL/+PH24+Tt6rewub6rrKWij4iBhpOUnZonICkuOzw1Mh8YERYDBA0KV1BZXktMRUJvaGFmc3R9eomOh4CVkpucsba/uK2qo6T5/vfw5eLr7MHGz8jd2tPUaW5nYHVye3xRVl9YTUpDRBkeFxAFAgsMISYvKD06MzROSUBHUlVcW3ZxeH9qbWRjPjkwNyIlLCsGAQgPGh0UE66poKeytby7lpGYn4qNhIPe2dDXwsXMy+bh6O/6/fTzAAAFgA+ACgAbgB4AFAARgDOANgA8ADmAKAAtgCeAIgBjgGYAbABpgHgAfYB3gHIAUABVgF+AWgBLgE4ARABBgMOAxgDMAMmA2ADdgNeA0gDwAPWA/4D6AOuA7gDkAOGAoAClgK+AqgC7gL4AtACxgJOAlgCcAJmAiACNgIeAggCDgYYBjAGJgZgBnYGXgZIBsAG1gb+BugGrga4BpAGhgeAB5YHvgeoB+4H+AfQB8YHTgdYB3AHZgcgBzYHHgcIBQAFFgU+BSgFbgV4BVAFRgXOBdgF8AXmBaAFtgWeBYgEjgSYBLAEpgTgBPYE3gTIBEAEVgR+BGgELgQ4BBAEBgQODBgMMAwmDGAMdgxeDEgMwAzWDP4M6AyuDLgMkAyGDYANlg2+DagN7g34DdANxg1ODVgNcA1mDSANNg0eDQgPAA8WDz4PKA9uD3gPUA9GD84P2A/wD+YPoA+2D54PiA6ODpgOsA6mDuAO9g7eDsgOQA5WDn4OaA4uDjgOEA4GDgAKFgo+CigKbgp4ClAKRgrOCtgK8ArmCqAKtgqeCogLjguYC7ALpgvgC/YL3gvIC0ALVgt+C2gLLgs4CxALBgkOCRgJMAkmCWAJdgleCUgJwAnWCf4J6AmuCbgJkAmGCIAIlgi+CKgI7gj4CNAIxghOCFgIcAhmCCAINggeCAgIAAAOGA4wACgOYAB4AFAOSA7AANgA8A7oAKAOuA6QAIgPgAGYAbAPqAHgD/gP0AHIAUAPWA9wAWgPIAE4ARAPCA0AAxgDMA0oA2ANeA1QA0gDwA3YDfAD6A2gA7gDkA2IAoAMmAywAqgM4AL4AtAMyAxAAlgCcAxoAiAMOAwQAggaABQYFDAaKBRgGngaUBRIFMAa2BrwFOgaoBS4FJAaiBWAG5gbsBWoG+AV+BXQG8gbQBVYFXAbaBUgGzgbEBUIFwAZGBkwFygZYBd4F1AZSBnAF9gX8BnoF6AZuBmQF4gYgBaYFrAYqBbgGPgY0BbIFkAYWBhwFmgYIBY4FhAYCCYAKBgoMCYoKGAmeCZQKEgowCbYJvAo6CagKLgokCaIKYAnmCewKagn4Cn4KdAnyCdAKVgpcCdoKSAnOCcQKQgrACUYJTArKCVgK3grUCVIJcAr2CvwJegroCW4JZAriCSAKpgqsCSoKuAk+CTQKsgqQCRYJHAqaCQgKjgqECQIPAAyGDIwPCgyYDx4PFAySDLAPNg88DLoPKAyuDKQPIgzgD2YPbAzqD3gM/gz0D3IPUAzWDNwPWgzID04PRAzCDEAPxg/MDEoP2AxeDFQP0g/wDHYMfA/6DGgP7g/kDGIPoAwmDCwPqgw4D74PtAwyDBAPlg+cDBoPiAwODAQPggAAF4ArgDwAU4BEAHgAb4CjgLQAiACfgPAA54DbgMwAQ4FUAWgBf4EQAQeBO4EsAeAB94HLgdwBs4GkAZgBj4GDgpQCqAK/gtACx4L7guwCIAI3gguCHAJzgmQCWAJPgsAD14Prg/wDk4OEA7gDr4Njg3QDSANfgzADJ4MbgwwDA4UUBSgFP4VQBUeFe4VsBaAFt4WLhZwF84XkBdgFz4VABFeEa4R8BBOEBAQ4BC+E44T0BMgE34SwBKeEm4SMBIAHl4erh7wH04fEB/gH74cjhzQHCAcfh3AHZ4dbh0wHw4bUBugG/4aQBoeGu4asBmAGd4ZLhlwGM4YkBhgGD4YDihQKKAo/ilAKR4p7imwKoAq3iouKnArziuQK2ArPikALV4tri3wLE4sECzgLL4vji/QLyAvfi7ALp4ubi4wLgAiXiKuIvAjTiMQI+AjviCOINAgICB+IcAhniFuITAjDidQJ6An/iZAJh4m7iawJYAl3iUuJXAkziSQJGAkPiQAPF48rjzwPU49ED3gPb4+jj7QPiA+fj/AP54/bj8wPQ45UDmgOf44QDgeOO44sDuAO947LjtwOs46kDpgOj46DjZQNqA2/jdANx437jewNIA03jQuNHA1zjWQNWA1PjcAM14zrjPwMk4yEDLgMr4xjjHQMSAxfjDAMJ4wbjAwMAAADlAOoADwD0ABEAHgD7AMgALQAiAMcAPADZANYAMwGQAXUBegGfAWQBgQGOAWsBWAG9AbIBVwGsAUkBRgGjAyADxQPKAy8D1AMxAz4D2wPoAw0DAgPnAxwD+QP2AxMCsAJVAloCvwJEAqECrgJLAngCnQKSAncCjAJpAmYCgwdgB4UHigdvB5QHcQd+B5sHqAdNB0IHpwdcB7kHtgdTBvAGFQYaBv8GBAbhBu4GCwY4Bt0G0gY3BswGKQYmBsMEQASlBKoETwS0BFEEXgS7BIgEbQRiBIcEfASZBJYEcwXQBTUFOgXfBSQFwQXOBSsFGAX9BfIFFwXsBQkFBgXjD+APBQ8KD+8PFA/xD/4PGw8oD80Pwg8nD9wPOQ82D9MOcA6VDpoOfw6EDmEObg6LDrgOXQ5SDrcOTA6pDqYOQwzADCUMKgzPDDQM0QzeDDsMCAztDOIMBwz8DBkMFgzzDVANtQ26DV8NpA1BDU4Nqw2YDX0Ncg2XDWwNiQ2GDWMIgAhlCGoIjwh0CJEIngh7CEgIrQiiCEcIvAhZCFYIswkQCfUJ+gkfCeQJAQkOCesJ2Ak9CTIJ1wksCckJxgkjC6ALRQtKC68LVAuxC74LWwtoC40LggtnC5wLeQt2C5MKMArVCtoKPwrECiEKLgrLCvgKHQoSCvcKDArpCuYKAwAAHuA84CIAOOBmAEQAWuBw4O4AzADS4MgAluC04KoAoOH+AdwBwuHYAYbhpOG6AZABDuEs4TIBKOF2AVQBSuFA494D/APi4/gDpuOE45oDsAMu4wzjEgMI41YDdANq42ACPuIc4gICGOJGAmQCeuJQ4s4C7ALy4ugCtuKU4ooCgOeeB7wHoue4B+bnxOfaB/AHbudM51IHSOcWBzQHKucgBn7mXOZCBljmBgYkBjrmEOaOBqwGsuaoBvbm1ObKBsAEXuR85GIEeOQmBAQEGuQw5K4EjASS5IgE1uT05OoE4OW+BZwFguWYBcbl5OX6BdAFTuVs5XIFaOU2BRQFCuUA7x4PPA8i7zgPZu9E71oPcA/u78zv0g/I75YPtA+q76AO/u7c7sIO2O6GDqQOuu6Q7g4OLA4y7igOdu5U7koOQAze7Pzs4gz47KYMhAya7LDsLgwMDBLsCAxW7HTsagxg7T4NHA0C7RgNRu1k7XoNUA3O7ezt8g3o7bYNlA2K7YAInui86KIIuOjmCMQI2ujw6G4ITAhS6EgIFug06CoIIOl+CVwJQulYCQbpJOk6CRAJjums6bIJqOn2CdQJyunA614LfAti63gLJusE6xoLMAuu64zrkguI69YL9Avq6+AKvuqc6oIKmOrGCuQK+urQ6k4KbApy6mgKNuoU6goKAAAA/gDcACIBuAFGAWQBmgJQAq4CjAJyA+gDFgM0A8oEoAReBHwEggUYBeYFxAU6BvAGDgYsBtIHSAe2B5QHaghgCJ4IvAhCCdgJJgkECfoKMArOCuwKEguIC3YLVAuqDMAMPgwcDOINeA2GDaQNWg6QDm4OTA6yDygP1g/0DwoR4BEeETwRwhBYEKYQhBB6E7ATThNsE5ISCBL2EtQSKhVAFb4VnBViFPgUBhQkFNoXEBfuF8wXMhaoFlYWdBaKGYAZfhlcGaIYOBjGGOQYGhvQGy4bDBvyGmgalhq0GkodIB3eHfwdAhyYHGYcRBy6H3Afjh+sH1IeyB42HhQe6iLgIh4iPCLCI1gjpiOEI3ogsCBOIGwgkiEIIfYh1CEqJkAmviacJmIn+CcGJyQn2iQQJO4kzCQyJaglViV0JYoqgCp+Klwqois4K8Yr5CsaKNAoLigMKPIpaCmWKbQpSi4gLt4u/C4CL5gvZi9EL7oscCyOLKwsUi3ILTYtFC3qMwAz/jPcMyIyuDJGMmQymjFQMa4xjDFyMOgwFjA0MMo3oDdeN3w3gjYYNuY2xDY6NfA1DjUsNdI0SDS2NJQ0ajtgO547vDtCOtg6JjoEOvo5MDnOOew5EjiIOHY4VDiqP8A/Pj8cP+I+eD6GPqQ+Wj2QPW49TD2yPCg81jz0PAoAAATgSOCMANDhFAFYAZzh4OIkAmgCrOLwAzTjeOO8A8DkRAQIBMzkkAVU5Rjl3AWgBmTmKObsBrDndAc4B/zngOiECMgIDOhQCZTp2OkcCWAKpOro6iwKcOu0C/gLPOtADMTsiOxMDBDt1A2YDVztIO7kDqgObO4wD/TvuO98DwDxBBFIEYzx0BAU8FjwnBDgEyTzaPOsE/DyNBJ4ErzywBVE9Qj1zBWQ9FQUGBTc9KD3ZBcoF+z3sBZ09jj2/BaAGYT5yPkMGVD4lBjYGBz4YPukG+gbLPtwGrT6+Po8GkD9xB2IHUz9EBzU/Jj8XBwgH+T/qP9sHzD+9B64Hnz+AMIEIkgijMLQIxTDWMOcI+AgJMBowKwg8ME0IXghvMHAJkTGCMbMJpDHVCcYJ9zHoMRkJCgk7MSwJXTFOMX8JYAqhMrIygwqUMuUK9grHMtgyKQo6CgsyHAptMn4yTwpQM7ELoguTM4QL9TPmM9cLyAs5MyozGwsMM30LbgtfM0AMwTTSNOMM9DSFDJYMpzS4NEkMWgxrNHwMDTQeNC8MMDXRDcIN8zXkDZU1hjW3DagNWTVKNXsNbDUdDQ4NPzUgNuEO8g7DNtQOpTa2NocOmA5pNno2Sw5cNi0OPg4PNhAP8TfiN9MPxDe1D6YPlzeIN3kPag9bN0wPPTcuNx8PAAAAYQDCAKMBhAHlAUYBJwMIA2kDygOrAowC7QJOAi8GWAY5BpoG+wfcB70HHgd/BVAFMQWSBfME1AS1BBYEdwz4DJkMOgxbDXwNHQ2+Dd8P8A+RDzIPUw50DhUOtg7XCqAKwQpiCgMLJAtFC+YLhwmoCckJagkLCCwITQjuCI8JuBnZGXoZGxg8GF0Y/hifGrAa0RpyGhMbNBtVG/Yblx/gH4EfIh9DHmQeBR6mHscc6ByJHCocSx1sHQ0drh3PFUAVIRWCFeMUxBSlFAYUZxZIFikWihbrF8wXrRcOF28TGBN5E9oTuxKcEv0SXhI/EBAQcRDSELMRlBH1EVYRNxM4M1kz+jObMrwy3TJ+Mh8wMDBRMPIwkzG0MdUxdjEXNWA1ATWiNcM05DSFNCY0RzZoNgk2qjbLN+w3jTcuN08/wD+hPwI/Yz5EPiU+hj7nPMg8qTwKPGs9TD0tPY497zmYOfk5Wjk7OBw4fTjeOL86kDrxOlI6MzsUO3U71ju3OoAq4SpCKiMrBCtlK8YrpymIKekpSikrKAwobSjOKK8s2Cy5LBosey1cLT0tni3/L9AvsS8SL3MuVC41LpYu9yZ4JhkmuibbJ/wnnSc+J18lcCURJbIl0yT0JJUkNiRXICAgQSDiIIMhpCHFIWYhByMoI0kj6iOLIqwizSJuIg8nJlZmVyZW5jZSBsaWJGTEFDIDEuMy4zIDIwMTkwODA0AGZMYUMAAABDYUxmIAAAABAAAAAQAAAAGAAAABgAAAAUAAAAAwAAAAUAAAAkAAAAIAAAAEAAAABAAAAAEAAAAEAAAAAIAAAAGAAAAEAAAAAIAAAAYAAAAAEAAAABAAAAbgAAAAgAAAAABAAAQAAAAAEAAAAXCAAACAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAAQAAAAcAAAAYAAAA/j8AAA4AAAABAAAAAQAAAAQAAAAEAAAABAAAAAMAAAABAAAACAAAABAAAAACAAAABAAAAAQAAAAFAAAABQAAAA8AAAAfAAAABAAAAAUAAAABAAAABgAAAAEAAAAAAAAAAgAAABAAAABAAAAAQ0QtREEgY3VlIHNoZWV0IG11c3QgaGF2ZSBhIGxlYWQtaW4gbGVuZ3RoIG9mIGF0IGxlYXN0IDIgc2Vjb25kcwBDRC1EQSBjdWUgc2hlZXQgbGVhZC1pbiBsZW5ndGggbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAGN1ZSBzaGVldCBtdXN0IGhhdmUgYXQgbGVhc3Qgb25lIHRyYWNrICh0aGUgbGVhZC1vdXQpAENELURBIGN1ZSBzaGVldCBtdXN0IGhhdmUgYSBsZWFkLW91dCB0cmFjayBudW1iZXIgMTcwICgweEFBKQBjdWUgc2hlZXQgbWF5IG5vdCBoYXZlIGEgdHJhY2sgbnVtYmVyIDAAQ0QtREEgY3VlIHNoZWV0IHRyYWNrIG51bWJlciBtdXN0IGJlIDEtOTkgb3IgMTcwAENELURBIGN1ZSBzaGVldCBsZWFkLW91dCBvZmZzZXQgbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAENELURBIGN1ZSBzaGVldCB0cmFjayBvZmZzZXQgbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAGN1ZSBzaGVldCB0cmFjayBtdXN0IGhhdmUgYXQgbGVhc3Qgb25lIGluZGV4IHBvaW50AGN1ZSBzaGVldCB0cmFjaydzIGZpcnN0IGluZGV4IG51bWJlciBtdXN0IGJlIDAgb3IgMQBDRC1EQSBjdWUgc2hlZXQgdHJhY2sgaW5kZXggb2Zmc2V0IG11c3QgYmUgZXZlbmx5IGRpdmlzaWJsZSBieSA1ODggc2FtcGxlcwBjdWUgc2hlZXQgdHJhY2sgaW5kZXggbnVtYmVycyBtdXN0IGluY3JlYXNlIGJ5IDEATUlNRSB0eXBlIHN0cmluZyBtdXN0IGNvbnRhaW4gb25seSBwcmludGFibGUgQVNDSUkgY2hhcmFjdGVycyAoMHgyMC0weDdlKQBkZXNjcmlwdGlvbiBzdHJpbmcgbXVzdCBiZSB2YWxpZCBVVEYtOA=="); +base64DecodeToExistingUint8Array(bufferView, 6516, "tx3BBG47ggnZJkMN3HYEE2trxReyTYYaBVBHHrjtCCYP8Mki1taKL2HLSytkmww104bNMQqgjjy9vU84cNsRTMfG0Ege4JNFqf1SQaytFV8bsNRbwpaXVnWLVlLINhlqfyvYbqYNm2MREFpnFEAdeaNd3H16e59wzWZedOC2I5hXq+Kcjo2hkTmQYJU8wCeLi93mj1L7pYLl5mSGWFsrvu9G6ro2YKm3gX1os4QtL60zMO6p6hatpF0LbKCQbTLUJ3Dz0P5WsN1JS3HZTBs2x/sG98MiILTOlT11yiiAOvKfnfv2Rru4+/Gmef/09j7hQ+v/5ZrNvOgt0H3sd3CGNMBtRzAZSwQ9rlbFOasGgiccG0MjxT0ALnIgwSrPnY4SeIBPFqGmDBsWu80fE+uKAaT2SwV90AgIys3JDAerl3iwtlZ8aZAVcd6N1HXb3ZNrbMBSb7XmEWIC+9Bmv0afXghbXlrRfR1XZmDcU2Mwm03ULVpJDQsZRLoW2ECXxqWsINtkqPn9J6VO4OahS7Chv/ytYLsliyO2kpbisi8rrYqYNmyOQRAvg/YN7ofzXamZREBonZ1mK5Aqe+qU5x204FAAdeSJJjbpPjv37TtrsPOMdnH3VVAy+uJN8/5f8LzG6O19wjHLPs+G1v/Lg4a41TSbedHtvTrcWqD72O7gDGlZ/c1tgNuOYDfGT2Qylgh6hYvJflytinPrsEt3Vg0ET+EQxUs4NoZGjytHQop7AFw9ZsFY5ECCVVNdQ1GeOx0lKSbcIfAAnyxHHV4oQk0ZNvVQ2DIsdps/m2taOybWFQORy9QHSO2XCv/wVg76oBEQTb3QFJSbkxkjhlIdDlYv8blL7vVgba3413Bs/NIgK+JlPermvBup6wsGaO+2uyfXAabm09iApd5vnWTaas0jxN3Q4sAE9qHNs+tgyX6NPr3JkP+5ELa8tKerfbCi+zquFeb7qszAuKd73XmjxmA2m3F995+oW7SSH0Z1lhoWMoitC/OMdC2wgcMwcYWZkIpdLo1LWferCFRAtslQReaOTvL7T0or3QxHnMDNQyF9gnuWYEN/T0YAcvhbwXb9C4ZoShZHbJMwBGEkLcVl6UubEV5WWhWHcBkYMG3YHDU9nwKCIF4GWwYdC+wb3A9RppM35rtSMz+dET6IgNA6jdCXJDrNViDj6xUtVPbUKXkmqcXOO2jBFx0rzKAA6silUK3WEk1s0strL998du7bwcuh43bWYOev8CPqGO3i7h29pfCqoGT0c4Yn+cSb5v0J/biJvuB5jWfGOoDQ2/uE1Yu8mmKWfZ67sD6TDK3/l7EQsK8GDXGr3ysypmg286JtZrS82nt1uANdNrW0QPexf0ZMQUMAAAAQAAAAiCoAAElEMw=="); +base64DecodeToExistingUint8Array(bufferView, 7576, "AQAAAAUAAAAYKw=="); +base64DecodeToExistingUint8Array(bufferView, 7600, "AwAAAAQAAAAEAAAABgAAAIP5ogBETm4A/CkVANFXJwDdNPUAYtvAADyZlQBBkEMAY1H+ALveqwC3YcUAOm4kANJNQgBJBuAACeouAByS0QDrHf4AKbEcAOg+pwD1NYIARLsuAJzphAC0JnAAQX5fANaROQBTgzkAnPQ5AItfhAAo+b0A+B87AN7/lwAPmAUAES/vAApaiwBtH20Az342AAnLJwBGT7cAnmY/AC3qXwC6J3UA5evHAD178QD3OQcAklKKAPtr6gAfsV8ACF2NADADVgB7/EYA8KtrACC8zwA29JoA46kdAF5hkQAIG+YAhZllAKAUXwCNQGgAgNj/ACdzTQAGBjEAylYVAMmocwB74mAAa4zAABnERwDNZ8MACejcAFmDKgCLdsQAphyWAESv3QAZV9EApT4FAAUH/wAzfj8AwjLoAJhP3gC7fTIAJj3DAB5r7wCf+F4ANR86AH/yygDxhx0AfJAhAGokfADVbvoAMC13ABU7QwC1FMYAwxmdAK3EwgAsTUEADABdAIZ9RgDjcS0Am8aaADNiAAC00nwAtKeXADdV1QDXPvYAoxAYAE12/ABknSoAcNerAGN8+AB6sFcAFxXnAMBJVgA71tkAp4Q4ACQjywDWincAWlQjAAAfuQDxChsAGc7fAJ8x/wBmHmoAmVdhAKz7RwB+f9gAImW3ADLoiQDmv2AA78TNAGw2CQBdP9QAFt7XAFg73gDem5IA0iIoACiG6ADiWE0AxsoyAAjjFgDgfcsAF8BQAPMdpwAY4FsALhM0AIMSYgCDSAEA9Y5bAK2wfwAe6fIASEpDABBn0wCq3dgArl9CAGphzgAKKKQA05m0AAam8gBcd38Ao8KDAGE8iACKc3gAr4xaAG/XvQAtpmMA9L/LAI2B7wAmwWcAVcpFAMrZNgAoqNIAwmGNABLJdwAEJhQAEkabAMRZxADIxUQATbKRAAAX8wDUQ60AKUnlAP3VEAAAvvwAHpTMAHDO7gATPvUA7PGAALPnwwDH+CgAkwWUAMFxPgAuCbMAC0XzAIgSnACrIHsALrWfAEeSwgB7Mi8ADFVtAHKnkABr5x8AMcuWAHkWSgBBeeIA9N+JAOiUlwDi5oQAmTGXAIjtawBfXzYAu/0OAEiatABnpGwAcXJCAI1dMgCfFbgAvOUJAI0xJQD3dDkAMAUcAA0MAQBLCGgALO5YAEeqkAB05wIAvdYkAPd9pgBuSHIAnxbvAI6UpgC0kfYA0VNRAM8K8gAgmDMA9Ut+ALJjaADdPl8AQF0DAIWJfwBVUikAN2TAAG3YEAAySDIAW0x1AE5x1ABFVG4ACwnBACr1aQAUZtUAJwedAF0EUAC0O9sA6nbFAIf5FwBJa30AHSe6AJZpKQDGzKwArRRUAJDiagCI2YkALHJQAASkvgB3B5QA8zBwAAD8JwDqcagAZsJJAGTgPQCX3YMAoz+XAEOU/QANhowAMUHeAJI5nQDdcIwAF7fnAAjfOwAVNysAXICgAFqAkwAQEZIAD+jYAGyArwDb/0sAOJAPAFkYdgBipRUAYcu7AMeJuQAQQL0A0vIEAEl1JwDrtvYA2yK7AAoUqgCJJi8AZIN2AAk7MwAOlBoAUTqqAB2jwgCv7a4AXCYSAG3CTQAtepwAwFaXAAM/gwAJ8PYAK0CMAG0xmQA5tAcADCAVANjDWwD1ksQAxq1LAE7KpQCnN80A5qk2AKuSlADdQmgAGWPeAHaM7wBoi1IA/Ns3AK6hqwDfFTEAAK6hAAz72gBkTWYA7QW3ACllMABXVr8AR/86AGr5uQB1vvMAKJPfAKuAMABmjPYABMsVAPoiBgDZ5B0APbOkAFcbjwA2zQkATkLpABO+pAAzI7UA8KoaAE9lqADSwaUACz8PAFt4zQAj+XYAe4sEAIkXcgDGplMAb27iAO/rAACbSlgAxNq3AKpmugB2z88A0QIdALHxLQCMmcEAw613AIZI2gD3XaAAxoD0AKzwLwDd7JoAP1y8ANDebQCQxx8AKtu2AKMlOgAAr5oArVOTALZXBAApLbQAS4B+ANoHpwB2qg4Ae1mhABYSKgDcty0A+uX9AInb/gCJvv0A5HZsAAap/AA+gHAAhW4VAP2H/wAoPgcAYWczACoYhgBNveoAs+evAI9tbgCVZzkAMb9bAITXSAAw3xYAxy1DACVhNQDJcM4AMMu4AL9s/QCkAKIABWzkAFrdoAAhb0cAYhLSALlchABwYUkAa1bgAJlSAQBQVTcAHtW3ADPxxAATbl8AXTDkAIUuqQAdssMAoTI2AAi3pADqsdQAFvchAI9p5AAn/3cADAOAAI1ALQBPzaAAIKWZALOi0wAvXQoAtPlCABHaywB9vtAAm9vBAKsXvQDKooEACGpcAC5VFwAnAFUAfxTwAOEHhgAUC2QAlkGNAIe+3gDa/SoAayW2AHuJNAAF8/4Aub+eAGhqTwBKKqgAT8RaAC34vADXWpgA9MeVAA1NjQAgOqYApFdfABQ/sQCAOJUAzCABAHHdhgDJ3rYAv2D1AE1lEQABB2sAjLCsALLA0ABRVUgAHvsOAJVywwCjBjsAwEA1AAbcewDgRcwATin6ANbKyADo80EAfGTeAJtk2ADZvjEApJfDAHdY1ABp48UA8NoTALo6PABGGEYAVXVfANK99QBuksYArC5dAA5E7QAcPkIAYcSHACn96QDn1vMAInzKAG+RNQAI4MUA/9eNAG5q4gCw/cYAkwjBAHxddABrrbIAzW6dAD5yewDGEWoA98+pAClz3wC1yboAtwBRAOKyDQB0uiQA5X1gAHTYigANFSwAgRgMAH5mlAABKRYAn3p2AP39vgBWRe8A2X42AOzZEwCLurkAxJf8ADGoJwDxbsMAlMU2ANioVgC0qLUAz8wOABKJLQBvVzQALFaJAJnO4wDWILkAa16qAD4qnAARX8wA/QtKAOH0+wCOO20A4oYsAOnUhAD8tKkA7+7RAC41yQAvOWEAOCFEABvZyACB/AoA+0pqAC8c2ABTtIQATpmMAFQizAAqVdwAwMbWAAsZlgAacLgAaZVkACZaYAA/Uu4AfxEPAPS1EQD8y/UANLwtADS87gDoXcwA3V5gAGeOmwCSM+8AyRe4AGFYmwDhV7wAUYPGANg+EADdcUgALRzdAK8YoQAhLEYAWfPXANl6mACeVMAAT4b6AFYG/ADlea4AiSI2ADitIgBnk9wAVeiqAIImOADK55sAUQ2kAJkzsQCp1w4AaQVIAGWy8AB/iKcAiEyXAPnRNgAhkrMAe4JKAJjPIQBAn9wA3EdVAOF0OgBn60IA/p3fAF7UXwB7Z6QAuqx6AFX2ogAriCMAQbpVAFluCAAhKoYAOUeDAInj5gDlntQASftAAP9W6QAcD8oAxVmKAJT6KwDTwcUAD8XPANtargBHxYYAhUNiACGGOwAseZQAEGGHACpMewCALBoAQ78SAIgmkAB4PIkAqMTkAOXbewDEOsIAJvTqAPdnigANkr8AZaMrAD2TsQC9fAsApFHcACfdYwBp4d0AmpQZAKgplQBozigACe20AESfIABOmMoAcIJjAH58IwAPuTIAp/WOABRW5wAh8QgAtZ0qAG9+TQClGVEAtfmrAILf1gCW3WEAFjYCAMQ6nwCDoqEAcu1tADmNegCCuKkAazJcAEYnWwAANO0A0gB3APz0VQABWU0A4HGA"); +base64DecodeToExistingUint8Array(bufferView, 10387, "QPsh+T8AAAAALUR0PgAAAICYRvg8AAAAYFHMeDsAAACAgxvwOQAAAEAgJXo4AAAAgCKC4zYAAAAAHfNpNQAAAAAAAOA/AAAAAAAA4L8BAAAAAgAAAAQAAAAFAAAABgAAAGluZmluaXR5AG5hbg=="); +base64DecodeToExistingUint8Array(bufferView, 10512, "0XSeAFedvSqAcFIP//8+JwoAAABkAAAA6AMAABAnAACghgEAQEIPAICWmAAA4fUFGAAAADUAAABxAAAAa////877//+Sv///YmFydGxldHQAYmFydGxldHRfaGFubgBibGFja21hbgBibGFja21hbl9oYXJyaXNfNHRlcm1fOTJkYgBjb25uZXMAZmxhdHRvcABnYXVzcygAaGFtbWluZwBoYW5uAGthaXNlcl9iZXNzZWwAbnV0dGFsbAByZWN0YW5nbGUAdHJpYW5nbGUAdHVrZXkoAHBhcnRpYWxfdHVrZXkoAHB1bmNob3V0X3R1a2V5KAB3ZWxjaABpbWFnZS9wbmcALS0+AHR1a2V5KDVlLTEpAHR1a2V5KDVlLTEpO3BhcnRpYWxfdHVrZXkoMikAdHVrZXkoNWUtMSk7cGFydGlhbF90dWtleSgyKTtwdW5jaG91dF90dWtleSgzKQ=="); +base64DecodeToExistingUint8Array(bufferView, 10881, "FQAAcR0AAAk="); +base64DecodeToExistingUint8Array(bufferView, 10900, "Ag=="); +base64DecodeToExistingUint8Array(bufferView, 10920, "AwAAAAAAAAAEAAAASC8AAAAE"); +base64DecodeToExistingUint8Array(bufferView, 10964, "/////w=="); +base64DecodeToExistingUint8Array(bufferView, 11032, "BQ=="); +base64DecodeToExistingUint8Array(bufferView, 11044, "CQ=="); +base64DecodeToExistingUint8Array(bufferView, 11068, "CgAAAAsAAABYMwAAAAQ="); +base64DecodeToExistingUint8Array(bufferView, 11092, "AQ=="); +base64DecodeToExistingUint8Array(bufferView, 11107, "Cv////8="); +base64DecodeToExistingUint8Array(bufferView, 11176, "GCs="); +base64DecodeToExistingUint8Array(bufferView, 11216, "AwAAAAAAAAAZKgAAAQAAAAE="); +base64DecodeToExistingUint8Array(bufferView, 11260, "AwAAAAAAAAAZKgAAAQ=="); +base64DecodeToExistingUint8Array(bufferView, 11304, "AwAAAAAAAAAZKg=="); +base64DecodeToExistingUint8Array(bufferView, 11324, "Bg=="); +base64DecodeToExistingUint8Array(bufferView, 11348, "BAAAAAAAAAAZKgAAAQAAAAEAAAAI"); +base64DecodeToExistingUint8Array(bufferView, 11392, "BAAAAAAAAAAZKgAAAQAAAAAAAAAI"); +base64DecodeToExistingUint8Array(bufferView, 11436, "BQAAAAAAAAAZKgAAAQAAAAAAAAAI"); +base64DecodeToExistingUint8Array(bufferView, 11480, "BgAAAAAAAAAlKgAAAQAAAAAAAAAM"); +base64DecodeToExistingUint8Array(bufferView, 11524, "BgAAAAAAAAAlKgAAAQAAAAAAAAAM"); +base64DecodeToExistingUint8Array(bufferView, 11568, "BgAAAAAAAABCKg=="); +return asmFunc({ + 'Int8Array': Int8Array, + 'Int16Array': Int16Array, + 'Int32Array': Int32Array, + 'Uint8Array': Uint8Array, + 'Uint16Array': Uint16Array, + 'Uint32Array': Uint32Array, + 'Float32Array': Float32Array, + 'Float64Array': Float64Array, + 'NaN': NaN, + 'Infinity': Infinity, + 'Math': Math + }, + asmLibraryArg, + wasmMemory.buffer +) + +} +)(asmLibraryArg, wasmMemory, wasmTable); + }, + + instantiate: /** @suppress{checkTypes} */ function(binary, info) { + return { + then: function(ok) { + ok({ + 'instance': new WebAssembly.Instance(new WebAssembly.Module(binary)) + }); + } + }; + }, + + RuntimeError: Error +}; + +// We don't need to actually download a wasm binary, mark it as present but empty. +wasmBinary = []; + + + +if (typeof WebAssembly !== 'object') { + abort('no native wasm support detected'); +} + + + + +// In MINIMAL_RUNTIME, setValue() and getValue() are only available when building with safe heap enabled, for heap safety checking. +// In traditional runtime, setValue() and getValue() are always available (although their use is highly discouraged due to perf penalties) + +/** @param {number} ptr + @param {number} value + @param {string} type + @param {number|boolean=} noSafe */ +function setValue(ptr, value, type, noSafe) { + type = type || 'i8'; + if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit + switch(type) { + case 'i1': HEAP8[((ptr)>>0)]=value; break; + case 'i8': HEAP8[((ptr)>>0)]=value; break; + case 'i16': HEAP16[((ptr)>>1)]=value; break; + case 'i32': HEAP32[((ptr)>>2)]=value; break; + case 'i64': (tempI64 = [value>>>0,(tempDouble=value,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((ptr)>>2)]=tempI64[0],HEAP32[(((ptr)+(4))>>2)]=tempI64[1]); break; + case 'float': HEAPF32[((ptr)>>2)]=value; break; + case 'double': HEAPF64[((ptr)>>3)]=value; break; + default: abort('invalid type for setValue: ' + type); + } +} + +/** @param {number} ptr + @param {string} type + @param {number|boolean=} noSafe */ +function getValue(ptr, type, noSafe) { + type = type || 'i8'; + if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit + switch(type) { + case 'i1': return HEAP8[((ptr)>>0)]; + case 'i8': return HEAP8[((ptr)>>0)]; + case 'i16': return HEAP16[((ptr)>>1)]; + case 'i32': return HEAP32[((ptr)>>2)]; + case 'i64': return HEAP32[((ptr)>>2)]; + case 'float': return HEAPF32[((ptr)>>2)]; + case 'double': return HEAPF64[((ptr)>>3)]; + default: abort('invalid type for getValue: ' + type); + } + return null; +} + + + + + + +// Wasm globals + +var wasmMemory; + +// In fastcomp asm.js, we don't need a wasm Table at all. +// In the wasm backend, we polyfill the WebAssembly object, +// so this creates a (non-native-wasm) table for us. +var wasmTable = new WebAssembly.Table({ + 'initial': 22, + 'maximum': 22 + 5, + 'element': 'anyfunc' +}); + + +//======================================== +// Runtime essentials +//======================================== + +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS = 0; + +/** @type {function(*, string=)} */ +function assert(condition, text) { + if (!condition) { + abort('Assertion failed: ' + text); + } +} + +// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) +function getCFunc(ident) { + var func = Module['_' + ident]; // closure exported function + assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); + return func; +} + +// C calling interface. +/** @param {string|null=} returnType + @param {Array=} argTypes + @param {Arguments|Array=} args + @param {Object=} opts */ +function ccall(ident, returnType, argTypes, args, opts) { + // For fast lookup of conversion functions + var toC = { + 'string': function(str) { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { // null string + // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' + var len = (str.length << 2) + 1; + ret = stackAlloc(len); + stringToUTF8(str, ret, len); + } + return ret; + }, + 'array': function(arr) { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + + function convertReturnValue(ret) { + if (returnType === 'string') return UTF8ToString(ret); + if (returnType === 'boolean') return Boolean(ret); + return ret; + } + + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func.apply(null, cArgs); + + ret = convertReturnValue(ret); + if (stack !== 0) stackRestore(stack); + return ret; +} + +/** @param {string=} returnType + @param {Array=} argTypes + @param {Object=} opts */ +function cwrap(ident, returnType, argTypes, opts) { + argTypes = argTypes || []; + // When the function takes numbers and returns a number, we can just return + // the original function + var numericArgs = argTypes.every(function(type){ return type === 'number'}); + var numericRet = returnType !== 'string'; + if (numericRet && numericArgs && !opts) { + return getCFunc(ident); + } + return function() { + return ccall(ident, returnType, argTypes, arguments, opts); + } +} + +var ALLOC_NORMAL = 0; // Tries to use _malloc() +var ALLOC_STACK = 1; // Lives for the duration of the current function call +var ALLOC_DYNAMIC = 2; // Cannot be freed except through sbrk +var ALLOC_NONE = 3; // Do not allocate + +// allocate(): This is for internal use. You can use it yourself as well, but the interface +// is a little tricky (see docs right below). The reason is that it is optimized +// for multiple syntaxes to save space in generated code. So you should +// normally not use allocate(), and instead allocate memory using _malloc(), +// initialize it with setValue(), and so forth. +// @slab: An array of data, or a number. If a number, then the size of the block to allocate, +// in *bytes* (note that this is sometimes confusing: the next parameter does not +// affect this!) +// @types: Either an array of types, one for each byte (or 0 if no type at that position), +// or a single type which is used for the entire block. This only matters if there +// is initial data - if @slab is a number, then this does not matter at all and is +// ignored. +// @allocator: How to allocate memory, see ALLOC_* +/** @type {function((TypedArray|Array|number), string, number, number=)} */ +function allocate(slab, types, allocator, ptr) { + var zeroinit, size; + if (typeof slab === 'number') { + zeroinit = true; + size = slab; + } else { + zeroinit = false; + size = slab.length; + } + + var singleType = typeof types === 'string' ? types : null; + + var ret; + if (allocator == ALLOC_NONE) { + ret = ptr; + } else { + ret = [_malloc, + stackAlloc, + dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)); + } + + if (zeroinit) { + var stop; + ptr = ret; + assert((ret & 3) == 0); + stop = ret + (size & ~3); + for (; ptr < stop; ptr += 4) { + HEAP32[((ptr)>>2)]=0; + } + stop = ret + size; + while (ptr < stop) { + HEAP8[((ptr++)>>0)]=0; + } + return ret; + } + + if (singleType === 'i8') { + if (slab.subarray || slab.slice) { + HEAPU8.set(/** @type {!Uint8Array} */ (slab), ret); + } else { + HEAPU8.set(new Uint8Array(slab), ret); + } + return ret; + } + + var i = 0, type, typeSize, previousType; + while (i < size) { + var curr = slab[i]; + + type = singleType || types[i]; + if (type === 0) { + i++; + continue; + } + + if (type == 'i64') type = 'i32'; // special case: we have one i32 here, and one i32 later + + setValue(ret+i, curr, type); + + // no need to look up size unless type changes, so cache it + if (previousType !== type) { + typeSize = getNativeTypeSize(type); + previousType = type; + } + i += typeSize; + } + + return ret; +} + +// Allocate memory during any stage of startup - static memory early on, dynamic memory later, malloc when ready +function getMemory(size) { + if (!runtimeInitialized) return dynamicAlloc(size); + return _malloc(size); +} + + + + +// runtime_strings.js: Strings related runtime functions that are part of both MINIMAL_RUNTIME and regular runtime. + +// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the given array that contains uint8 values, returns +// a copy of that string as a Javascript String object. + +var UTF8Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf8') : undefined; + +/** + * @param {number} idx + * @param {number=} maxBytesToRead + * @return {string} + */ +function UTF8ArrayToString(heap, idx, maxBytesToRead) { + var endIdx = idx + maxBytesToRead; + var endPtr = idx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. + // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. + // (As a tiny code save trick, compare endPtr against endIdx using a negation, so that undefined means Infinity) + while (heap[endPtr] && !(endPtr >= endIdx)) ++endPtr; + + if (endPtr - idx > 16 && heap.subarray && UTF8Decoder) { + return UTF8Decoder.decode(heap.subarray(idx, endPtr)); + } else { + var str = ''; + // If building with TextDecoder, we have already computed the string length above, so test loop end condition against that + while (idx < endPtr) { + // For UTF8 byte structure, see: + // http://en.wikipedia.org/wiki/UTF-8#Description + // https://www.ietf.org/rfc/rfc2279.txt + // https://tools.ietf.org/html/rfc3629 + var u0 = heap[idx++]; + if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } + var u1 = heap[idx++] & 63; + if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } + var u2 = heap[idx++] & 63; + if ((u0 & 0xF0) == 0xE0) { + u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; + } else { + u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heap[idx++] & 63); + } + + if (u0 < 0x10000) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } + } + } + return str; +} + +// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the emscripten HEAP, returns a +// copy of that string as a Javascript String object. +// maxBytesToRead: an optional length that specifies the maximum number of bytes to read. You can omit +// this parameter to scan the string until the first \0 byte. If maxBytesToRead is +// passed, and the string at [ptr, ptr+maxBytesToReadr[ contains a null byte in the +// middle, then the string will cut short at that byte index (i.e. maxBytesToRead will +// not produce a string of exact length [ptr, ptr+maxBytesToRead[) +// N.B. mixing frequent uses of UTF8ToString() with and without maxBytesToRead may +// throw JS JIT optimizations off, so it is worth to consider consistently using one +// style or the other. +/** + * @param {number} ptr + * @param {number=} maxBytesToRead + * @return {string} + */ +function UTF8ToString(ptr, maxBytesToRead) { + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; +} + +// Copies the given Javascript String object 'str' to the given byte array at address 'outIdx', +// encoded in UTF8 form and null-terminated. The copy will require at most str.length*4+1 bytes of space in the HEAP. +// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// heap: the array to copy to. Each index in this array is assumed to be one 8-byte element. +// outIdx: The starting offset in the array to begin the copying. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. +// This count should include the null terminator, +// i.e. if maxBytesToWrite=1, only the null terminator will be written and nothing else. +// maxBytesToWrite=0 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) { + if (!(maxBytesToWrite > 0)) // Parameter maxBytesToWrite is not optional. Negative values, 0, null, undefined and false each don't write out any bytes. + return 0; + + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description and https://www.ietf.org/rfc/rfc2279.txt and https://tools.ietf.org/html/rfc3629 + var u = str.charCodeAt(i); // possibly a lead surrogate + if (u >= 0xD800 && u <= 0xDFFF) { + var u1 = str.charCodeAt(++i); + u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); + } + if (u <= 0x7F) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 0x7FF) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 0xC0 | (u >> 6); + heap[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 0xE0 | (u >> 12); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 0xF0 | (u >> 18); + heap[outIdx++] = 0x80 | ((u >> 12) & 63); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF8 form. The copy will require at most str.length*4+1 bytes of space in the HEAP. +// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF8(str, outPtr, maxBytesToWrite) { + return stringToUTF8Array(str, HEAPU8,outPtr, maxBytesToWrite); +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF8 byte array, EXCLUDING the null terminator byte. +function lengthBytesUTF8(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var u = str.charCodeAt(i); // possibly a lead surrogate + if (u >= 0xD800 && u <= 0xDFFF) u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + if (u <= 0x7F) ++len; + else if (u <= 0x7FF) len += 2; + else if (u <= 0xFFFF) len += 3; + else len += 4; + } + return len; +} + + + + + +// runtime_strings_extra.js: Strings related runtime functions that are available only in regular runtime. + +// Given a pointer 'ptr' to a null-terminated ASCII-encoded string in the emscripten HEAP, returns +// a copy of that string as a Javascript String object. + +function AsciiToString(ptr) { + var str = ''; + while (1) { + var ch = HEAPU8[((ptr++)>>0)]; + if (!ch) return str; + str += String.fromCharCode(ch); + } +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in ASCII form. The copy will require at most str.length+1 bytes of space in the HEAP. + +function stringToAscii(str, outPtr) { + return writeAsciiToMemory(str, outPtr, false); +} + +// Given a pointer 'ptr' to a null-terminated UTF16LE-encoded string in the emscripten HEAP, returns +// a copy of that string as a Javascript String object. + +var UTF16Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : undefined; + +function UTF16ToString(ptr, maxBytesToRead) { + var endPtr = ptr; + // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. + // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. + var idx = endPtr >> 1; + var maxIdx = idx + maxBytesToRead / 2; + // If maxBytesToRead is not passed explicitly, it will be undefined, and this + // will always evaluate to true. This saves on code size. + while (!(idx >= maxIdx) && HEAPU16[idx]) ++idx; + endPtr = idx << 1; + + if (endPtr - ptr > 32 && UTF16Decoder) { + return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); + } else { + var i = 0; + + var str = ''; + while (1) { + var codeUnit = HEAP16[(((ptr)+(i*2))>>1)]; + if (codeUnit == 0 || i == maxBytesToRead / 2) return str; + ++i; + // fromCharCode constructs a character from a UTF-16 code unit, so we can pass the UTF16 string right through. + str += String.fromCharCode(codeUnit); + } + } +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF16 form. The copy will require at most str.length*4+2 bytes of space in the HEAP. +// Use the function lengthBytesUTF16() to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// outPtr: Byte address in Emscripten HEAP where to write the string to. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null +// terminator, i.e. if maxBytesToWrite=2, only the null terminator will be written and nothing else. +// maxBytesToWrite<2 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF16(str, outPtr, maxBytesToWrite) { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 0x7FFFFFFF; + } + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; // Null terminator. + var startPtr = outPtr; + var numCharsToWrite = (maxBytesToWrite < str.length*2) ? (maxBytesToWrite / 2) : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. + var codeUnit = str.charCodeAt(i); // possibly a lead surrogate + HEAP16[((outPtr)>>1)]=codeUnit; + outPtr += 2; + } + // Null-terminate the pointer to the HEAP. + HEAP16[((outPtr)>>1)]=0; + return outPtr - startPtr; +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. + +function lengthBytesUTF16(str) { + return str.length*2; +} + +function UTF32ToString(ptr, maxBytesToRead) { + var i = 0; + + var str = ''; + // If maxBytesToRead is not passed explicitly, it will be undefined, and this + // will always evaluate to true. This saves on code size. + while (!(i >= maxBytesToRead / 4)) { + var utf32 = HEAP32[(((ptr)+(i*4))>>2)]; + if (utf32 == 0) break; + ++i; + // Gotcha: fromCharCode constructs a character from a UTF-16 encoded code (pair), not from a Unicode code point! So encode the code point to UTF-16 for constructing. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + if (utf32 >= 0x10000) { + var ch = utf32 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } else { + str += String.fromCharCode(utf32); + } + } + return str; +} + +// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', +// null-terminated and encoded in UTF32 form. The copy will require at most str.length*4+4 bytes of space in the HEAP. +// Use the function lengthBytesUTF32() to compute the exact number of bytes (excluding null terminator) that this function will write. +// Parameters: +// str: the Javascript string to copy. +// outPtr: Byte address in Emscripten HEAP where to write the string to. +// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null +// terminator, i.e. if maxBytesToWrite=4, only the null terminator will be written and nothing else. +// maxBytesToWrite<4 does not write any bytes to the output, not even the null terminator. +// Returns the number of bytes written, EXCLUDING the null terminator. + +function stringToUTF32(str, outPtr, maxBytesToWrite) { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 0x7FFFFFFF; + } + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var codeUnit = str.charCodeAt(i); // possibly a lead surrogate + if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) { + var trailSurrogate = str.charCodeAt(++i); + codeUnit = 0x10000 + ((codeUnit & 0x3FF) << 10) | (trailSurrogate & 0x3FF); + } + HEAP32[((outPtr)>>2)]=codeUnit; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + // Null-terminate the pointer to the HEAP. + HEAP32[((outPtr)>>2)]=0; + return outPtr - startPtr; +} + +// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. + +function lengthBytesUTF32(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var codeUnit = str.charCodeAt(i); + if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) ++i; // possibly a lead surrogate, so skip over the tail surrogate. + len += 4; + } + + return len; +} + +// Allocate heap space for a JS string, and write it there. +// It is the responsibility of the caller to free() that memory. +function allocateUTF8(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8Array(str, HEAP8, ret, size); + return ret; +} + +// Allocate stack space for a JS string, and write it there. +function allocateUTF8OnStack(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8Array(str, HEAP8, ret, size); + return ret; +} + +// Deprecated: This function should not be called because it is unsafe and does not provide +// a maximum length limit of how many bytes it is allowed to write. Prefer calling the +// function stringToUTF8Array() instead, which takes in a maximum length that can be used +// to be secure from out of bounds writes. +/** @deprecated + @param {boolean=} dontAddNull */ +function writeStringToMemory(string, buffer, dontAddNull) { + warnOnce('writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!'); + + var /** @type {number} */ lastChar, /** @type {number} */ end; + if (dontAddNull) { + // stringToUTF8Array always appends null. If we don't want to do that, remember the + // character that existed at the location where the null will be placed, and restore + // that after the write (below). + end = buffer + lengthBytesUTF8(string); + lastChar = HEAP8[end]; + } + stringToUTF8(string, buffer, Infinity); + if (dontAddNull) HEAP8[end] = lastChar; // Restore the value under the null character. +} + +function writeArrayToMemory(array, buffer) { + HEAP8.set(array, buffer); +} + +/** @param {boolean=} dontAddNull */ +function writeAsciiToMemory(str, buffer, dontAddNull) { + for (var i = 0; i < str.length; ++i) { + HEAP8[((buffer++)>>0)]=str.charCodeAt(i); + } + // Null-terminate the pointer to the HEAP. + if (!dontAddNull) HEAP8[((buffer)>>0)]=0; +} + + + +// Memory management + +var PAGE_SIZE = 16384; +var WASM_PAGE_SIZE = 65536; +var ASMJS_PAGE_SIZE = 16777216; + +function alignUp(x, multiple) { + if (x % multiple > 0) { + x += multiple - (x % multiple); + } + return x; +} + +var HEAP, +/** @type {ArrayBuffer} */ + buffer, +/** @type {Int8Array} */ + HEAP8, +/** @type {Uint8Array} */ + HEAPU8, +/** @type {Int16Array} */ + HEAP16, +/** @type {Uint16Array} */ + HEAPU16, +/** @type {Int32Array} */ + HEAP32, +/** @type {Uint32Array} */ + HEAPU32, +/** @type {Float32Array} */ + HEAPF32, +/** @type {Float64Array} */ + HEAPF64; + +function updateGlobalBufferAndViews(buf) { + buffer = buf; + Module['HEAP8'] = HEAP8 = new Int8Array(buf); + Module['HEAP16'] = HEAP16 = new Int16Array(buf); + Module['HEAP32'] = HEAP32 = new Int32Array(buf); + Module['HEAPU8'] = HEAPU8 = new Uint8Array(buf); + Module['HEAPU16'] = HEAPU16 = new Uint16Array(buf); + Module['HEAPU32'] = HEAPU32 = new Uint32Array(buf); + Module['HEAPF32'] = HEAPF32 = new Float32Array(buf); + Module['HEAPF64'] = HEAPF64 = new Float64Array(buf); +} + +var STATIC_BASE = 1024, + STACK_BASE = 5257216, + STACKTOP = STACK_BASE, + STACK_MAX = 14336, + DYNAMIC_BASE = 5257216, + DYNAMICTOP_PTR = 14176; + + + +var TOTAL_STACK = 5242880; + +var INITIAL_INITIAL_MEMORY = Module['INITIAL_MEMORY'] || 16777216; + + + + + + + + + +// In non-standalone/normal mode, we create the memory here. + + + +// Create the main memory. (Note: this isn't used in STANDALONE_WASM mode since the wasm +// memory is created in the wasm, not in JS.) + + if (Module['wasmMemory']) { + wasmMemory = Module['wasmMemory']; + } else + { + wasmMemory = new WebAssembly.Memory({ + 'initial': INITIAL_INITIAL_MEMORY / WASM_PAGE_SIZE + , + 'maximum': 2147483648 / WASM_PAGE_SIZE + }); + } + + +if (wasmMemory) { + buffer = wasmMemory.buffer; +} + +// If the user provides an incorrect length, just use that length instead rather than providing the user to +// specifically provide the memory length with Module['INITIAL_MEMORY']. +INITIAL_INITIAL_MEMORY = buffer.byteLength; +updateGlobalBufferAndViews(buffer); + +HEAP32[DYNAMICTOP_PTR>>2] = DYNAMIC_BASE; + + + + + + + + + + + + + + +function callRuntimeCallbacks(callbacks) { + while(callbacks.length > 0) { + var callback = callbacks.shift(); + if (typeof callback == 'function') { + callback(Module); // Pass the module as the first argument. + continue; + } + var func = callback.func; + if (typeof func === 'number') { + if (callback.arg === undefined) { + Module['dynCall_v'](func); + } else { + Module['dynCall_vi'](func, callback.arg); + } + } else { + func(callback.arg === undefined ? null : callback.arg); + } + } +} + +var __ATPRERUN__ = []; // functions called before the runtime is initialized +var __ATINIT__ = []; // functions called during startup +var __ATMAIN__ = []; // functions called when main() is to be run +var __ATEXIT__ = []; // functions called during shutdown +var __ATPOSTRUN__ = []; // functions called after the main() is called + +var runtimeInitialized = false; +var runtimeExited = false; + + +function preRun() { + + if (Module['preRun']) { + if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; + while (Module['preRun'].length) { + addOnPreRun(Module['preRun'].shift()); + } + } + + callRuntimeCallbacks(__ATPRERUN__); +} + +function initRuntime() { + runtimeInitialized = true; + if (!Module["noFSInit"] && !FS.init.initialized) FS.init(); +TTY.init(); + callRuntimeCallbacks(__ATINIT__); +} + +function preMain() { + FS.ignorePermissions = false; + callRuntimeCallbacks(__ATMAIN__); +} + +function exitRuntime() { + runtimeExited = true; +} + +function postRun() { + + if (Module['postRun']) { + if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; + while (Module['postRun'].length) { + addOnPostRun(Module['postRun'].shift()); + } + } + + callRuntimeCallbacks(__ATPOSTRUN__); +} + +function addOnPreRun(cb) { + __ATPRERUN__.unshift(cb); +} + +function addOnInit(cb) { + __ATINIT__.unshift(cb); +} + +function addOnPreMain(cb) { + __ATMAIN__.unshift(cb); +} + +function addOnExit(cb) { +} + +function addOnPostRun(cb) { + __ATPOSTRUN__.unshift(cb); +} + +/** @param {number|boolean=} ignore */ +function unSign(value, bits, ignore) { + if (value >= 0) { + return value; + } + return bits <= 32 ? 2*Math.abs(1 << (bits-1)) + value // Need some trickery, since if bits == 32, we are right at the limit of the bits JS uses in bitshifts + : Math.pow(2, bits) + value; +} +/** @param {number|boolean=} ignore */ +function reSign(value, bits, ignore) { + if (value <= 0) { + return value; + } + var half = bits <= 32 ? Math.abs(1 << (bits-1)) // abs is needed if bits == 32 + : Math.pow(2, bits-1); + if (value >= half && (bits <= 32 || value > half)) { // for huge values, we can hit the precision limit and always get true here. so don't do that + // but, in general there is no perfect solution here. With 64-bit ints, we get rounding and errors + // TODO: In i64 mode 1, resign the two parts separately and safely + value = -2*half + value; // Cannot bitshift half, as it may be at the limit of the bits JS uses in bitshifts + } + return value; +} + + + + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc + + +var Math_abs = Math.abs; +var Math_cos = Math.cos; +var Math_sin = Math.sin; +var Math_tan = Math.tan; +var Math_acos = Math.acos; +var Math_asin = Math.asin; +var Math_atan = Math.atan; +var Math_atan2 = Math.atan2; +var Math_exp = Math.exp; +var Math_log = Math.log; +var Math_sqrt = Math.sqrt; +var Math_ceil = Math.ceil; +var Math_floor = Math.floor; +var Math_pow = Math.pow; +var Math_imul = Math.imul; +var Math_fround = Math.fround; +var Math_round = Math.round; +var Math_min = Math.min; +var Math_max = Math.max; +var Math_clz32 = Math.clz32; +var Math_trunc = Math.trunc; + + + +// A counter of dependencies for calling run(). If we need to +// do asynchronous work before running, increment this and +// decrement it. Incrementing must happen in a place like +// Module.preRun (used by emcc to add file preloading). +// Note that you can add dependencies in preRun, even though +// it happens right before run - run will be postponed until +// the dependencies are met. +var runDependencies = 0; +var runDependencyWatcher = null; +var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled + +function getUniqueRunDependency(id) { + return id; +} + +function addRunDependency(id) { + runDependencies++; + + if (Module['monitorRunDependencies']) { + Module['monitorRunDependencies'](runDependencies); + } + +} + +function removeRunDependency(id) { + runDependencies--; + + if (Module['monitorRunDependencies']) { + Module['monitorRunDependencies'](runDependencies); + } + + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } +} + +Module["preloadedImages"] = {}; // maps url to image data +Module["preloadedAudios"] = {}; // maps url to audio data + +/** @param {string|number=} what */ +function abort(what) { + if (Module['onAbort']) { + Module['onAbort'](what); + } + + what += ''; + out(what); + err(what); + + ABORT = true; + EXITSTATUS = 1; + + what = 'abort(' + what + '). Build with -s ASSERTIONS=1 for more info.'; + + // Throw a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + throw new WebAssembly.RuntimeError(what); +} + + +var memoryInitializer = null; + + + + + + + + + + + + +function hasPrefix(str, prefix) { + return String.prototype.startsWith ? + str.startsWith(prefix) : + str.indexOf(prefix) === 0; +} + +// Prefix of data URIs emitted by SINGLE_FILE and related options. +var dataURIPrefix = 'data:application/octet-stream;base64,'; + +// Indicates whether filename is a base64 data URI. +function isDataURI(filename) { + return hasPrefix(filename, dataURIPrefix); +} + +var fileURIPrefix = "file://"; + +// Indicates whether filename is delivered via file protocol (as opposed to http/https) +function isFileURI(filename) { + return hasPrefix(filename, fileURIPrefix); +} + + + + +var wasmBinaryFile = 'libflac.wasm'; +if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile); +} + +function getBinary() { + try { + if (wasmBinary) { + return new Uint8Array(wasmBinary); + } + + var binary = tryParseAsDataURI(wasmBinaryFile); + if (binary) { + return binary; + } + if (readBinary) { + return readBinary(wasmBinaryFile); + } else { + throw "both async and sync fetching of the wasm failed"; + } + } + catch (err) { + abort(err); + } +} + +function getBinaryPromise() { + // If we don't have the binary yet, and have the Fetch api, use that; + // in some environments, like Electron's render process, Fetch api may be present, but have a different context than expected, let's only use it on the Web + if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === 'function' + // Let's not use fetch to get objects over file:// as it's most likely Cordova which doesn't support fetch for file:// + && !isFileURI(wasmBinaryFile) + ) { + return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) { + if (!response['ok']) { + throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; + } + return response['arrayBuffer'](); + }).catch(function () { + return getBinary(); + }); + } + // Otherwise, getBinary should be able to get it synchronously + return new Promise(function(resolve, reject) { + resolve(getBinary()); + }); +} + + + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +function createWasm() { + // prepare imports + var info = { + 'env': asmLibraryArg, + 'wasi_snapshot_preview1': asmLibraryArg + }; + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ + function receiveInstance(instance, module) { + var exports = instance.exports; + Module['asm'] = exports; + removeRunDependency('wasm-instantiate'); + } + // we can't run yet (except in a pthread, where we have a custom sync instantiator) + addRunDependency('wasm-instantiate'); + + + function receiveInstantiatedSource(output) { + // 'output' is a WebAssemblyInstantiatedSource object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above USE_PTHREADS-enabled path. + receiveInstance(output['instance']); + } + + + function instantiateArrayBuffer(receiver) { + return getBinaryPromise().then(function(binary) { + return WebAssembly.instantiate(binary, info); + }).then(receiver, function(reason) { + err('failed to asynchronously prepare wasm: ' + reason); + + + abort(reason); + }); + } + + // Prefer streaming instantiation if available. + function instantiateAsync() { + if (!wasmBinary && + typeof WebAssembly.instantiateStreaming === 'function' && + !isDataURI(wasmBinaryFile) && + // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously. + !isFileURI(wasmBinaryFile) && + typeof fetch === 'function') { + fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) { + var result = WebAssembly.instantiateStreaming(response, info); + return result.then(receiveInstantiatedSource, function(reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err('wasm streaming compile failed: ' + reason); + err('falling back to ArrayBuffer instantiation'); + return instantiateArrayBuffer(receiveInstantiatedSource); + }); + }); + } else { + return instantiateArrayBuffer(receiveInstantiatedSource); + } + } + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel + // to any other async startup actions they are performing. + if (Module['instantiateWasm']) { + try { + var exports = Module['instantiateWasm'](info, receiveInstance); + return exports; + } catch(e) { + err('Module.instantiateWasm callback failed with error: ' + e); + return false; + } + } + + instantiateAsync(); + return {}; // no exports yet; we'll fill them in later +} + + +// Globals used by JS i64 conversions +var tempDouble; +var tempI64; + +// === Body === + +var ASM_CONSTS = { + +}; + + + + +// STATICTOP = STATIC_BASE + 13312; +/* global initializers */ __ATINIT__.push({ func: function() { ___wasm_call_ctors() } }); + + + + +/* no memory initializer */ +// {{PRE_LIBRARY}} + + + function demangle(func) { + return func; + } + + function demangleAll(text) { + var regex = + /\b_Z[\w\d_]+/g; + return text.replace(regex, + function(x) { + var y = demangle(x); + return x === y ? x : (y + ' [' + x + ']'); + }); + } + + function jsStackTrace() { + var err = new Error(); + if (!err.stack) { + // IE10+ special cases: It does have callstack info, but it is only populated if an Error object is thrown, + // so try that as a special-case. + try { + throw new Error(); + } catch(e) { + err = e; + } + if (!err.stack) { + return '(no stack trace available)'; + } + } + return err.stack.toString(); + } + + function stackTrace() { + var js = jsStackTrace(); + if (Module['extraStackTrace']) js += '\n' + Module['extraStackTrace'](); + return demangleAll(js); + } + + function _emscripten_get_sbrk_ptr() { + return 14176; + } + + function _emscripten_memcpy_big(dest, src, num) { + HEAPU8.copyWithin(dest, src, src + num); + } + + + function _emscripten_get_heap_size() { + return HEAPU8.length; + } + + function emscripten_realloc_buffer(size) { + try { + // round size grow request up to wasm page size (fixed 64KB per spec) + wasmMemory.grow((size - buffer.byteLength + 65535) >>> 16); // .grow() takes a delta compared to the previous size + updateGlobalBufferAndViews(wasmMemory.buffer); + return 1 /*success*/; + } catch(e) { + } + }function _emscripten_resize_heap(requestedSize) { + requestedSize = requestedSize >>> 0; + var oldSize = _emscripten_get_heap_size(); + // With pthreads, races can happen (another thread might increase the size in between), so return a failure, and let the caller retry. + + + var PAGE_MULTIPLE = 65536; + + // Memory resize rules: + // 1. When resizing, always produce a resized heap that is at least 16MB (to avoid tiny heap sizes receiving lots of repeated resizes at startup) + // 2. Always increase heap size to at least the requested size, rounded up to next page multiple. + // 3a. If MEMORY_GROWTH_LINEAR_STEP == -1, excessively resize the heap geometrically: increase the heap size according to + // MEMORY_GROWTH_GEOMETRIC_STEP factor (default +20%), + // At most overreserve by MEMORY_GROWTH_GEOMETRIC_CAP bytes (default 96MB). + // 3b. If MEMORY_GROWTH_LINEAR_STEP != -1, excessively resize the heap linearly: increase the heap size by at least MEMORY_GROWTH_LINEAR_STEP bytes. + // 4. Max size for the heap is capped at 2048MB-PAGE_MULTIPLE, or by MAXIMUM_MEMORY, or by ASAN limit, depending on which is smallest + // 5. If we were unable to allocate as much memory, it may be due to over-eager decision to excessively reserve due to (3) above. + // Hence if an allocation fails, cut down on the amount of excess growth, in an attempt to succeed to perform a smaller allocation. + + // A limit was set for how much we can grow. We should not exceed that + // (the wasm binary specifies it, so if we tried, we'd fail anyhow). + var maxHeapSize = 2147483648; + if (requestedSize > maxHeapSize) { + return false; + } + + var minHeapSize = 16777216; + + // Loop through potential heap size increases. If we attempt a too eager reservation that fails, cut down on the + // attempted size and reserve a smaller bump instead. (max 3 times, chosen somewhat arbitrarily) + for(var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); // ensure geometric growth + // but limit overreserving (default to capping at +96MB overgrowth at most) + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296 ); + + + var newSize = Math.min(maxHeapSize, alignUp(Math.max(minHeapSize, requestedSize, overGrownHeapSize), PAGE_MULTIPLE)); + + var replacement = emscripten_realloc_buffer(newSize); + if (replacement) { + + return true; + } + } + return false; + } + + + + var PATH={splitPath:function(filename) { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1); + },normalizeArray:function(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up; up--) { + parts.unshift('..'); + } + } + return parts; + },normalize:function(path) { + var isAbsolute = path.charAt(0) === '/', + trailingSlash = path.substr(-1) === '/'; + // Normalize the path + path = PATH.normalizeArray(path.split('/').filter(function(p) { + return !!p; + }), !isAbsolute).join('/'); + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + return (isAbsolute ? '/' : '') + path; + },dirname:function(path) { + var result = PATH.splitPath(path), + root = result[0], + dir = result[1]; + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + return root + dir; + },basename:function(path) { + // EMSCRIPTEN return '/'' for '/', not an empty string + if (path === '/') return '/'; + var lastSlash = path.lastIndexOf('/'); + if (lastSlash === -1) return path; + return path.substr(lastSlash+1); + },extname:function(path) { + return PATH.splitPath(path)[3]; + },join:function() { + var paths = Array.prototype.slice.call(arguments, 0); + return PATH.normalize(paths.join('/')); + },join2:function(l, r) { + return PATH.normalize(l + '/' + r); + }}; + + + function setErrNo(value) { + HEAP32[((___errno_location())>>2)]=value; + return value; + } + + var PATH_FS={resolve:function() { + var resolvedPath = '', + resolvedAbsolute = false; + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + return ''; // an invalid portion invalidates the whole thing + } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter(function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + },relative:function(from, to) { + from = PATH_FS.resolve(from).substr(1); + to = PATH_FS.resolve(to).substr(1); + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join('/'); + }}; + + var TTY={ttys:[],init:function () { + // https://github.com/emscripten-core/emscripten/pull/1555 + // if (ENVIRONMENT_IS_NODE) { + // // currently, FS.init does not distinguish if process.stdin is a file or TTY + // // device, it always assumes it's a TTY device. because of this, we're forcing + // // process.stdin to UTF8 encoding to at least make stdin reading compatible + // // with text files until FS.init can be refactored. + // process['stdin']['setEncoding']('utf8'); + // } + },shutdown:function() { + // https://github.com/emscripten-core/emscripten/pull/1555 + // if (ENVIRONMENT_IS_NODE) { + // // inolen: any idea as to why node -e 'process.stdin.read()' wouldn't exit immediately (with process.stdin being a tty)? + // // isaacs: because now it's reading from the stream, you've expressed interest in it, so that read() kicks off a _read() which creates a ReadReq operation + // // inolen: I thought read() in that case was a synchronous operation that just grabbed some amount of buffered data if it exists? + // // isaacs: it is. but it also triggers a _read() call, which calls readStart() on the handle + // // isaacs: do process.stdin.pause() and i'd think it'd probably close the pending call + // process['stdin']['pause'](); + // } + },register:function(dev, ops) { + TTY.ttys[dev] = { input: [], output: [], ops: ops }; + FS.registerDevice(dev, TTY.stream_ops); + },stream_ops:{open:function(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43); + } + stream.tty = tty; + stream.seekable = false; + },close:function(stream) { + // flush any pending line data + stream.tty.ops.flush(stream.tty); + },flush:function(stream) { + stream.tty.ops.flush(stream.tty); + },read:function(stream, buffer, offset, length, pos /* ignored */) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60); + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset+i] = result; + } + if (bytesRead) { + stream.node.timestamp = Date.now(); + } + return bytesRead; + },write:function(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60); + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset+i]); + } + } catch (e) { + throw new FS.ErrnoError(29); + } + if (length) { + stream.node.timestamp = Date.now(); + } + return i; + }},default_tty_ops:{get_char:function(tty) { + if (!tty.input.length) { + var result = null; + if (ENVIRONMENT_IS_NODE) { + // we will read data by chunks of BUFSIZE + var BUFSIZE = 256; + var buf = Buffer.alloc ? Buffer.alloc(BUFSIZE) : new Buffer(BUFSIZE); + var bytesRead = 0; + + try { + bytesRead = nodeFS.readSync(process.stdin.fd, buf, 0, BUFSIZE, null); + } catch(e) { + // Cross-platform differences: on Windows, reading EOF throws an exception, but on other OSes, + // reading EOF returns 0. Uniformize behavior by treating the EOF exception to return 0. + if (e.toString().indexOf('EOF') != -1) bytesRead = 0; + else throw e; + } + + if (bytesRead > 0) { + result = buf.slice(0, bytesRead).toString('utf-8'); + } else { + result = null; + } + } else + if (typeof window != 'undefined' && + typeof window.prompt == 'function') { + // Browser. + result = window.prompt('Input: '); // returns null on cancel + if (result !== null) { + result += '\n'; + } + } else if (typeof readline == 'function') { + // Command line. + result = readline(); + if (result !== null) { + result += '\n'; + } + } + if (!result) { + return null; + } + tty.input = intArrayFromString(result, true); + } + return tty.input.shift(); + },put_char:function(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output, 0)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); // val == 0 would cut text output off in the middle. + } + },flush:function(tty) { + if (tty.output && tty.output.length > 0) { + out(UTF8ArrayToString(tty.output, 0)); + tty.output = []; + } + }},default_tty1_ops:{put_char:function(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output, 0)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + },flush:function(tty) { + if (tty.output && tty.output.length > 0) { + err(UTF8ArrayToString(tty.output, 0)); + tty.output = []; + } + }}}; + + var MEMFS={ops_table:null,mount:function(mount) { + return MEMFS.createNode(null, '/', 16384 | 511 /* 0777 */, 0); + },createNode:function(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + // no supported + throw new FS.ErrnoError(63); + } + if (!MEMFS.ops_table) { + MEMFS.ops_table = { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + allocate: MEMFS.stream_ops.allocate, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; + } + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {}; + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + node.usedBytes = 0; // The actual number of bytes used in the typed array, as opposed to contents.length which gives the whole capacity. + // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred + // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size + // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. + node.contents = null; + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream; + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream; + } + node.timestamp = Date.now(); + // add the new node to the parent + if (parent) { + parent.contents[name] = node; + } + return node; + },getFileDataAsRegularArray:function(node) { + if (node.contents && node.contents.subarray) { + var arr = []; + for (var i = 0; i < node.usedBytes; ++i) arr.push(node.contents[i]); + return arr; // Returns a copy of the original data. + } + return node.contents; // No-op, the file contents are already in a JS array. Return as-is. + },getFileDataAsTypedArray:function(node) { + if (!node.contents) return new Uint8Array(0); + if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. + return new Uint8Array(node.contents); + },expandFileStorage:function(node, newCapacity) { + var prevCapacity = node.contents ? node.contents.length : 0; + if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. + // Don't expand strictly to the given requested limit if it's only a very small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for large sizes, do a much more conservative size*1.125 increase to + // avoid overshooting the allocation cap by a very large margin. + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); + if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. + var oldContents = node.contents; + node.contents = new Uint8Array(newCapacity); // Allocate new storage. + if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); // Copy old data over to the new storage. + return; + },resizeFileStorage:function(node, newSize) { + if (node.usedBytes == newSize) return; + if (newSize == 0) { + node.contents = null; // Fully decommit when requesting a resize to zero. + node.usedBytes = 0; + return; + } + if (!node.contents || node.contents.subarray) { // Resize a typed array if that is being used as the backing store. + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); // Allocate new storage. + if (oldContents) { + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. + } + node.usedBytes = newSize; + return; + } + // Backing with a JS array. + if (!node.contents) node.contents = []; + if (node.contents.length > newSize) node.contents.length = newSize; + else while (node.contents.length < newSize) node.contents.push(0); + node.usedBytes = newSize; + },node_ops:{getattr:function(node) { + var attr = {}; + // device numbers reuse inode numbers. + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096; + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes; + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length; + } else { + attr.size = 0; + } + attr.atime = new Date(node.timestamp); + attr.mtime = new Date(node.timestamp); + attr.ctime = new Date(node.timestamp); + // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), + // but this is not required by the standard. + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr; + },setattr:function(node, attr) { + if (attr.mode !== undefined) { + node.mode = attr.mode; + } + if (attr.timestamp !== undefined) { + node.timestamp = attr.timestamp; + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size); + } + },lookup:function(parent, name) { + throw FS.genericErrors[44]; + },mknod:function(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev); + },rename:function(old_node, new_dir, new_name) { + // if we're overwriting a directory at new_name, make sure it's empty. + if (FS.isDir(old_node.mode)) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) { + } + if (new_node) { + for (var i in new_node.contents) { + throw new FS.ErrnoError(55); + } + } + } + // do the internal rewiring + delete old_node.parent.contents[old_node.name]; + old_node.name = new_name; + new_dir.contents[new_name] = old_node; + old_node.parent = new_dir; + },unlink:function(parent, name) { + delete parent.contents[name]; + },rmdir:function(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55); + } + delete parent.contents[name]; + },readdir:function(node) { + var entries = ['.', '..']; + for (var key in node.contents) { + if (!node.contents.hasOwnProperty(key)) { + continue; + } + entries.push(key); + } + return entries; + },symlink:function(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 511 /* 0777 */ | 40960, 0); + node.link = oldpath; + return node; + },readlink:function(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28); + } + return node.link; + }},stream_ops:{read:function(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + if (size > 8 && contents.subarray) { // non-trivial, and typed array + buffer.set(contents.subarray(position, position + size), offset); + } else { + for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i]; + } + return size; + },write:function(stream, buffer, offset, length, position, canOwn) { + // If the buffer is located in main memory (HEAP), and if + // memory can grow, we can't hold on to references of the + // memory buffer, as they may get invalidated. That means we + // need to do copy its contents. + if (buffer.buffer === HEAP8.buffer) { + canOwn = false; + } + + if (!length) return 0; + var node = stream.node; + node.timestamp = Date.now(); + + if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array? + if (canOwn) { + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + return length; + } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + return length; + } else if (position + length <= node.usedBytes) { // Writing to an already allocated and used subrange of the file? + node.contents.set(buffer.subarray(offset, offset + length), position); + return length; + } + } + + // Appending to an existing file and we need to reallocate, or source data did not come as a typed array. + MEMFS.expandFileStorage(node, position+length); + if (node.contents.subarray && buffer.subarray) node.contents.set(buffer.subarray(offset, offset + length), position); // Use typed array write if available. + else { + for (var i = 0; i < length; i++) { + node.contents[position + i] = buffer[offset + i]; // Or fall back to manual write if not. + } + } + node.usedBytes = Math.max(node.usedBytes, position + length); + return length; + },llseek:function(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes; + } + } + if (position < 0) { + throw new FS.ErrnoError(28); + } + return position; + },allocate:function(stream, offset, length) { + MEMFS.expandFileStorage(stream.node, offset + length); + stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length); + },mmap:function(stream, address, length, position, prot, flags) { + // We don't currently support location hints for the address of the mapping + assert(address === 0); + + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + var ptr; + var allocated; + var contents = stream.node.contents; + // Only make a new copy when MAP_PRIVATE is specified. + if (!(flags & 2) && contents.buffer === buffer) { + // We can't emulate MAP_SHARED when the file is not backed by the buffer + // we're mapping to (e.g. the HEAP buffer). + allocated = false; + ptr = contents.byteOffset; + } else { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + allocated = true; + ptr = _malloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + HEAP8.set(contents, ptr); + } + return { ptr: ptr, allocated: allocated }; + },msync:function(stream, buffer, offset, length, mmapFlags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (mmapFlags & 2) { + // MAP_PRIVATE calls need not to be synced back to underlying fs + return 0; + } + + var bytesWritten = MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + // should we check if bytesWritten and length are the same? + return 0; + }}};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,trackingDelegate:{},tracking:{openFlags:{READ:1,WRITE:2}},ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,handleFSError:function(e) { + if (!(e instanceof FS.ErrnoError)) throw e + ' : ' + stackTrace(); + return setErrNo(e.errno); + },lookupPath:function(path, opts) { + path = PATH_FS.resolve(FS.cwd(), path); + opts = opts || {}; + + if (!path) return { path: '', node: null }; + + var defaults = { + follow_mount: true, + recurse_count: 0 + }; + for (var key in defaults) { + if (opts[key] === undefined) { + opts[key] = defaults[key]; + } + } + + if (opts.recurse_count > 8) { // max recursive lookup of 8 + throw new FS.ErrnoError(32); + } + + // split the path + var parts = PATH.normalizeArray(path.split('/').filter(function(p) { + return !!p; + }), false); + + // start at the root + var current = FS.root; + var current_path = '/'; + + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length-1); + if (islast && opts.parent) { + // stop resolving + break; + } + + current = FS.lookupNode(current, parts[i]); + current_path = PATH.join2(current_path, parts[i]); + + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current)) { + if (!islast || (islast && opts.follow_mount)) { + current = current.mounted.root; + } + } + + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (!islast || opts.follow) { + var count = 0; + while (FS.isLink(current.mode)) { + var link = FS.readlink(current_path); + current_path = PATH_FS.resolve(PATH.dirname(current_path), link); + + var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count }); + current = lookup.node; + + if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). + throw new FS.ErrnoError(32); + } + } + } + } + + return { path: current_path, node: current }; + },getPath:function(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length-1] !== '/' ? mount + '/' + path : mount + path; + } + path = path ? node.name + '/' + path : node.name; + node = node.parent; + } + },hashName:function(parentid, name) { + var hash = 0; + + + for (var i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % FS.nameTable.length; + },hashAddNode:function(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node; + },hashRemoveNode:function(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next; + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break; + } + current = current.name_next; + } + } + },lookupNode:function(parent, name) { + var errCode = FS.mayLookup(parent); + if (errCode) { + throw new FS.ErrnoError(errCode, parent); + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node; + } + } + // if we failed to find it in the cache, call into the VFS + return FS.lookup(parent, name); + },createNode:function(parent, name, mode, rdev) { + var node = new FS.FSNode(parent, name, mode, rdev); + + FS.hashAddNode(node); + + return node; + },destroyNode:function(node) { + FS.hashRemoveNode(node); + },isRoot:function(node) { + return node === node.parent; + },isMountpoint:function(node) { + return !!node.mounted; + },isFile:function(mode) { + return (mode & 61440) === 32768; + },isDir:function(mode) { + return (mode & 61440) === 16384; + },isLink:function(mode) { + return (mode & 61440) === 40960; + },isChrdev:function(mode) { + return (mode & 61440) === 8192; + },isBlkdev:function(mode) { + return (mode & 61440) === 24576; + },isFIFO:function(mode) { + return (mode & 61440) === 4096; + },isSocket:function(mode) { + return (mode & 49152) === 49152; + },flagModes:{"r":0,"rs":1052672,"r+":2,"w":577,"wx":705,"xw":705,"w+":578,"wx+":706,"xw+":706,"a":1089,"ax":1217,"xa":1217,"a+":1090,"ax+":1218,"xa+":1218},modeStringToFlags:function(str) { + var flags = FS.flagModes[str]; + if (typeof flags === 'undefined') { + throw new Error('Unknown file open mode: ' + str); + } + return flags; + },flagsToPermissionString:function(flag) { + var perms = ['r', 'w', 'rw'][flag & 3]; + if ((flag & 512)) { + perms += 'w'; + } + return perms; + },nodePermissions:function(node, perms) { + if (FS.ignorePermissions) { + return 0; + } + // return 0 if any user, group or owner bits are set. + if (perms.indexOf('r') !== -1 && !(node.mode & 292)) { + return 2; + } else if (perms.indexOf('w') !== -1 && !(node.mode & 146)) { + return 2; + } else if (perms.indexOf('x') !== -1 && !(node.mode & 73)) { + return 2; + } + return 0; + },mayLookup:function(dir) { + var errCode = FS.nodePermissions(dir, 'x'); + if (errCode) return errCode; + if (!dir.node_ops.lookup) return 2; + return 0; + },mayCreate:function(dir, name) { + try { + var node = FS.lookupNode(dir, name); + return 20; + } catch (e) { + } + return FS.nodePermissions(dir, 'wx'); + },mayDelete:function(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name); + } catch (e) { + return e.errno; + } + var errCode = FS.nodePermissions(dir, 'wx'); + if (errCode) { + return errCode; + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54; + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10; + } + } else { + if (FS.isDir(node.mode)) { + return 31; + } + } + return 0; + },mayOpen:function(node, flags) { + if (!node) { + return 44; + } + if (FS.isLink(node.mode)) { + return 32; + } else if (FS.isDir(node.mode)) { + if (FS.flagsToPermissionString(flags) !== 'r' || // opening for write + (flags & 512)) { // TODO: check for O_SEARCH? (== search for dir only) + return 31; + } + } + return FS.nodePermissions(node, FS.flagsToPermissionString(flags)); + },MAX_OPEN_FDS:4096,nextfd:function(fd_start, fd_end) { + fd_start = fd_start || 0; + fd_end = fd_end || FS.MAX_OPEN_FDS; + for (var fd = fd_start; fd <= fd_end; fd++) { + if (!FS.streams[fd]) { + return fd; + } + } + throw new FS.ErrnoError(33); + },getStream:function(fd) { + return FS.streams[fd]; + },createStream:function(stream, fd_start, fd_end) { + if (!FS.FSStream) { + FS.FSStream = /** @constructor */ function(){}; + FS.FSStream.prototype = { + object: { + get: function() { return this.node; }, + set: function(val) { this.node = val; } + }, + isRead: { + get: function() { return (this.flags & 2097155) !== 1; } + }, + isWrite: { + get: function() { return (this.flags & 2097155) !== 0; } + }, + isAppend: { + get: function() { return (this.flags & 1024); } + } + }; + } + // clone it, so we can return an instance of FSStream + var newStream = new FS.FSStream(); + for (var p in stream) { + newStream[p] = stream[p]; + } + stream = newStream; + var fd = FS.nextfd(fd_start, fd_end); + stream.fd = fd; + FS.streams[fd] = stream; + return stream; + },closeStream:function(fd) { + FS.streams[fd] = null; + },chrdev_stream_ops:{open:function(stream) { + var device = FS.getDevice(stream.node.rdev); + // override node's stream ops with the device's + stream.stream_ops = device.stream_ops; + // forward the open call + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + },llseek:function() { + throw new FS.ErrnoError(70); + }},major:function(dev) { + return ((dev) >> 8); + },minor:function(dev) { + return ((dev) & 0xff); + },makedev:function(ma, mi) { + return ((ma) << 8 | (mi)); + },registerDevice:function(dev, ops) { + FS.devices[dev] = { stream_ops: ops }; + },getDevice:function(dev) { + return FS.devices[dev]; + },getMounts:function(mount) { + var mounts = []; + var check = [mount]; + + while (check.length) { + var m = check.pop(); + + mounts.push(m); + + check.push.apply(check, m.mounts); + } + + return mounts; + },syncfs:function(populate, callback) { + if (typeof(populate) === 'function') { + callback = populate; + populate = false; + } + + FS.syncFSRequests++; + + if (FS.syncFSRequests > 1) { + err('warning: ' + FS.syncFSRequests + ' FS.syncfs operations in flight at once, probably just doing extra work'); + } + + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + + function doCallback(errCode) { + FS.syncFSRequests--; + return callback(errCode); + } + + function done(errCode) { + if (errCode) { + if (!done.errored) { + done.errored = true; + return doCallback(errCode); + } + return; + } + if (++completed >= mounts.length) { + doCallback(null); + } + }; + + // sync all mounts + mounts.forEach(function (mount) { + if (!mount.type.syncfs) { + return done(null); + } + mount.type.syncfs(mount, populate, done); + }); + },mount:function(type, opts, mountpoint) { + var root = mountpoint === '/'; + var pseudo = !mountpoint; + var node; + + if (root && FS.root) { + throw new FS.ErrnoError(10); + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); + + mountpoint = lookup.path; // use the absolute path + node = lookup.node; + + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + } + + var mount = { + type: type, + opts: opts, + mountpoint: mountpoint, + mounts: [] + }; + + // create a root node for the fs + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + + if (root) { + FS.root = mountRoot; + } else if (node) { + // set as a mountpoint + node.mounted = mount; + + // add the new mount to the current mount's children + if (node.mount) { + node.mount.mounts.push(mount); + } + } + + return mountRoot; + },unmount:function (mountpoint) { + var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); + + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28); + } + + // destroy the nodes for this mount, and all its child mounts + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + + Object.keys(FS.nameTable).forEach(function (hash) { + var current = FS.nameTable[hash]; + + while (current) { + var next = current.name_next; + + if (mounts.indexOf(current.mount) !== -1) { + FS.destroyNode(current); + } + + current = next; + } + }); + + // no longer a mountpoint + node.mounted = null; + + // remove this mount from the child mounts + var idx = node.mount.mounts.indexOf(mount); + node.mount.mounts.splice(idx, 1); + },lookup:function(parent, name) { + return parent.node_ops.lookup(parent, name); + },mknod:function(path, mode, dev) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name || name === '.' || name === '..') { + throw new FS.ErrnoError(28); + } + var errCode = FS.mayCreate(parent, name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.mknod(parent, name, mode, dev); + },create:function(path, mode) { + mode = mode !== undefined ? mode : 438 /* 0666 */; + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0); + },mkdir:function(path, mode) { + mode = mode !== undefined ? mode : 511 /* 0777 */; + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0); + },mkdirTree:function(path, mode) { + var dirs = path.split('/'); + var d = ''; + for (var i = 0; i < dirs.length; ++i) { + if (!dirs[i]) continue; + d += '/' + dirs[i]; + try { + FS.mkdir(d, mode); + } catch(e) { + if (e.errno != 20) throw e; + } + } + },mkdev:function(path, mode, dev) { + if (typeof(dev) === 'undefined') { + dev = mode; + mode = 438 /* 0666 */; + } + mode |= 8192; + return FS.mknod(path, mode, dev); + },symlink:function(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44); + } + var lookup = FS.lookupPath(newpath, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.symlink(parent, newname, oldpath); + },rename:function(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + // parents must exist + var lookup, old_dir, new_dir; + try { + lookup = FS.lookupPath(old_path, { parent: true }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { parent: true }); + new_dir = lookup.node; + } catch (e) { + throw new FS.ErrnoError(10); + } + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + // need to be part of the same mount + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75); + } + // source must exist + var old_node = FS.lookupNode(old_dir, old_name); + // old path should not be an ancestor of the new path + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== '.') { + throw new FS.ErrnoError(28); + } + // new path should not be an ancestor of the old path + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== '.') { + throw new FS.ErrnoError(55); + } + // see if the new path already exists + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) { + // not fatal + } + // early out if nothing needs to change + if (old_node === new_node) { + return; + } + // we'll need to delete the old entry + var isdir = FS.isDir(old_node.mode); + var errCode = FS.mayDelete(old_dir, old_name, isdir); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // need delete permissions if we'll be overwriting. + // need create permissions if new doesn't already exist. + errCode = new_node ? + FS.mayDelete(new_dir, new_name, isdir) : + FS.mayCreate(new_dir, new_name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { + throw new FS.ErrnoError(10); + } + // if we are going to change the parent, check write permissions + if (new_dir !== old_dir) { + errCode = FS.nodePermissions(old_dir, 'w'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + try { + if (FS.trackingDelegate['willMovePath']) { + FS.trackingDelegate['willMovePath'](old_path, new_path); + } + } catch(e) { + err("FS.trackingDelegate['willMovePath']('"+old_path+"', '"+new_path+"') threw an exception: " + e.message); + } + // remove the node from the lookup hash + FS.hashRemoveNode(old_node); + // do the underlying fs rename + try { + old_dir.node_ops.rename(old_node, new_dir, new_name); + } catch (e) { + throw e; + } finally { + // add the node back to the hash (in case node_ops.rename + // changed its name) + FS.hashAddNode(old_node); + } + try { + if (FS.trackingDelegate['onMovePath']) FS.trackingDelegate['onMovePath'](old_path, new_path); + } catch(e) { + err("FS.trackingDelegate['onMovePath']('"+old_path+"', '"+new_path+"') threw an exception: " + e.message); + } + },rmdir:function(path) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, true); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + try { + if (FS.trackingDelegate['willDeletePath']) { + FS.trackingDelegate['willDeletePath'](path); + } + } catch(e) { + err("FS.trackingDelegate['willDeletePath']('"+path+"') threw an exception: " + e.message); + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + try { + if (FS.trackingDelegate['onDeletePath']) FS.trackingDelegate['onDeletePath'](path); + } catch(e) { + err("FS.trackingDelegate['onDeletePath']('"+path+"') threw an exception: " + e.message); + } + },readdir:function(path) { + var lookup = FS.lookupPath(path, { follow: true }); + var node = lookup.node; + if (!node.node_ops.readdir) { + throw new FS.ErrnoError(54); + } + return node.node_ops.readdir(node); + },unlink:function(path) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, false); + if (errCode) { + // According to POSIX, we should map EISDIR to EPERM, but + // we instead do what Linux does (and we must, as we use + // the musl linux libc). + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + try { + if (FS.trackingDelegate['willDeletePath']) { + FS.trackingDelegate['willDeletePath'](path); + } + } catch(e) { + err("FS.trackingDelegate['willDeletePath']('"+path+"') threw an exception: " + e.message); + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + try { + if (FS.trackingDelegate['onDeletePath']) FS.trackingDelegate['onDeletePath'](path); + } catch(e) { + err("FS.trackingDelegate['onDeletePath']('"+path+"') threw an exception: " + e.message); + } + },readlink:function(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44); + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28); + } + return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)); + },stat:function(path, dontFollow) { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + var node = lookup.node; + if (!node) { + throw new FS.ErrnoError(44); + } + if (!node.node_ops.getattr) { + throw new FS.ErrnoError(63); + } + return node.node_ops.getattr(node); + },lstat:function(path) { + return FS.stat(path, true); + },chmod:function(path, mode, dontFollow) { + var node; + if (typeof path === 'string') { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + node = lookup.node; + } else { + node = path; + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63); + } + node.node_ops.setattr(node, { + mode: (mode & 4095) | (node.mode & ~4095), + timestamp: Date.now() + }); + },lchmod:function(path, mode) { + FS.chmod(path, mode, true); + },fchmod:function(fd, mode) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + FS.chmod(stream.node, mode); + },chown:function(path, uid, gid, dontFollow) { + var node; + if (typeof path === 'string') { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + node = lookup.node; + } else { + node = path; + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63); + } + node.node_ops.setattr(node, { + timestamp: Date.now() + // we ignore the uid / gid for now + }); + },lchown:function(path, uid, gid) { + FS.chown(path, uid, gid, true); + },fchown:function(fd, uid, gid) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + FS.chown(stream.node, uid, gid); + },truncate:function(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path === 'string') { + var lookup = FS.lookupPath(path, { follow: true }); + node = lookup.node; + } else { + node = path; + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63); + } + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31); + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28); + } + var errCode = FS.nodePermissions(node, 'w'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + node.node_ops.setattr(node, { + size: len, + timestamp: Date.now() + }); + },ftruncate:function(fd, len) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28); + } + FS.truncate(stream.node, len); + },utime:function(path, atime, mtime) { + var lookup = FS.lookupPath(path, { follow: true }); + var node = lookup.node; + node.node_ops.setattr(node, { + timestamp: Math.max(atime, mtime) + }); + },open:function(path, flags, mode, fd_start, fd_end) { + if (path === "") { + throw new FS.ErrnoError(44); + } + flags = typeof flags === 'string' ? FS.modeStringToFlags(flags) : flags; + mode = typeof mode === 'undefined' ? 438 /* 0666 */ : mode; + if ((flags & 64)) { + mode = (mode & 4095) | 32768; + } else { + mode = 0; + } + var node; + if (typeof path === 'object') { + node = path; + } else { + path = PATH.normalize(path); + try { + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072) + }); + node = lookup.node; + } catch (e) { + // ignore + } + } + // perhaps we need to create the node + var created = false; + if ((flags & 64)) { + if (node) { + // if O_CREAT and O_EXCL are set, error out if the node already exists + if ((flags & 128)) { + throw new FS.ErrnoError(20); + } + } else { + // node doesn't exist, try to create it + node = FS.mknod(path, mode, 0); + created = true; + } + } + if (!node) { + throw new FS.ErrnoError(44); + } + // can't truncate a device + if (FS.isChrdev(node.mode)) { + flags &= ~512; + } + // if asked only for a directory, then this must be one + if ((flags & 65536) && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + // check permissions, if this is not a file we just created now (it is ok to + // create and write to a file with read-only permissions; it is read-only + // for later use) + if (!created) { + var errCode = FS.mayOpen(node, flags); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // do truncation if necessary + if ((flags & 512)) { + FS.truncate(node, 0); + } + // we've already handled these, don't pass down to the underlying vfs + flags &= ~(128 | 512 | 131072); + + // register the stream with the filesystem + var stream = FS.createStream({ + node: node, + path: FS.getPath(node), // we want the absolute path to the node + flags: flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + // used by the file family libc calls (fopen, fwrite, ferror, etc.) + ungotten: [], + error: false + }, fd_start, fd_end); + // call the new stream's open function + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + if (Module['logReadFiles'] && !(flags & 1)) { + if (!FS.readFiles) FS.readFiles = {}; + if (!(path in FS.readFiles)) { + FS.readFiles[path] = 1; + err("FS.trackingDelegate error on read file: " + path); + } + } + try { + if (FS.trackingDelegate['onOpenFile']) { + var trackingFlags = 0; + if ((flags & 2097155) !== 1) { + trackingFlags |= FS.tracking.openFlags.READ; + } + if ((flags & 2097155) !== 0) { + trackingFlags |= FS.tracking.openFlags.WRITE; + } + FS.trackingDelegate['onOpenFile'](path, trackingFlags); + } + } catch(e) { + err("FS.trackingDelegate['onOpenFile']('"+path+"', flags) threw an exception: " + e.message); + } + return stream; + },close:function(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (stream.getdents) stream.getdents = null; // free readdir state + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream); + } + } catch (e) { + throw e; + } finally { + FS.closeStream(stream.fd); + } + stream.fd = null; + },isClosed:function(stream) { + return stream.fd === null; + },llseek:function(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70); + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28); + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position; + },read:function(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28); + } + var seeking = typeof position !== 'undefined'; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead; + },write:function(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28); + } + if (stream.seekable && stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position !== 'undefined'; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + try { + if (stream.path && FS.trackingDelegate['onWriteToFile']) FS.trackingDelegate['onWriteToFile'](stream.path); + } catch(e) { + err("FS.trackingDelegate['onWriteToFile']('"+stream.path+"') threw an exception: " + e.message); + } + return bytesWritten; + },allocate:function(stream, offset, length) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (offset < 0 || length <= 0) { + throw new FS.ErrnoError(28); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (!stream.stream_ops.allocate) { + throw new FS.ErrnoError(138); + } + stream.stream_ops.allocate(stream, offset, length); + },mmap:function(stream, address, length, position, prot, flags) { + // User requests writing to file (prot & PROT_WRITE != 0). + // Checking if we have permissions to write to the file unless + // MAP_PRIVATE flag is set. According to POSIX spec it is possible + // to write to file opened in read-only mode with MAP_PRIVATE flag, + // as all modifications will be visible only in the memory of + // the current process. + if ((prot & 2) !== 0 + && (flags & 2) === 0 + && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2); + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43); + } + return stream.stream_ops.mmap(stream, address, length, position, prot, flags); + },msync:function(stream, buffer, offset, length, mmapFlags) { + if (!stream || !stream.stream_ops.msync) { + return 0; + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); + },munmap:function(stream) { + return 0; + },ioctl:function(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59); + } + return stream.stream_ops.ioctl(stream, cmd, arg); + },readFile:function(path, opts) { + opts = opts || {}; + opts.flags = opts.flags || 'r'; + opts.encoding = opts.encoding || 'binary'; + if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { + throw new Error('Invalid encoding type "' + opts.encoding + '"'); + } + var ret; + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === 'utf8') { + ret = UTF8ArrayToString(buf, 0); + } else if (opts.encoding === 'binary') { + ret = buf; + } + FS.close(stream); + return ret; + },writeFile:function(path, data, opts) { + opts = opts || {}; + opts.flags = opts.flags || 'w'; + var stream = FS.open(path, opts.flags, opts.mode); + if (typeof data === 'string') { + var buf = new Uint8Array(lengthBytesUTF8(data)+1); + var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); + FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn); + } else if (ArrayBuffer.isView(data)) { + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); + } else { + throw new Error('Unsupported data type'); + } + FS.close(stream); + },cwd:function() { + return FS.currentPath; + },chdir:function(path) { + var lookup = FS.lookupPath(path, { follow: true }); + if (lookup.node === null) { + throw new FS.ErrnoError(44); + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54); + } + var errCode = FS.nodePermissions(lookup.node, 'x'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.currentPath = lookup.path; + },createDefaultDirectories:function() { + FS.mkdir('/tmp'); + FS.mkdir('/home'); + FS.mkdir('/home/web_user'); + },createDefaultDevices:function() { + // create /dev + FS.mkdir('/dev'); + // setup /dev/null + FS.registerDevice(FS.makedev(1, 3), { + read: function() { return 0; }, + write: function(stream, buffer, offset, length, pos) { return length; } + }); + FS.mkdev('/dev/null', FS.makedev(1, 3)); + // setup /dev/tty and /dev/tty1 + // stderr needs to print output using Module['printErr'] + // so we register a second tty just for it. + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev('/dev/tty', FS.makedev(5, 0)); + FS.mkdev('/dev/tty1', FS.makedev(6, 0)); + // setup /dev/[u]random + var random_device; + if (typeof crypto === 'object' && typeof crypto['getRandomValues'] === 'function') { + // for modern web browsers + var randomBuffer = new Uint8Array(1); + random_device = function() { crypto.getRandomValues(randomBuffer); return randomBuffer[0]; }; + } else + if (ENVIRONMENT_IS_NODE) { + // for nodejs with or without crypto support included + try { + var crypto_module = require('crypto'); + // nodejs has crypto support + random_device = function() { return crypto_module['randomBytes'](1)[0]; }; + } catch (e) { + // nodejs doesn't have crypto support + } + } else + {} + if (!random_device) { + // we couldn't find a proper implementation, as Math.random() is not suitable for /dev/random, see emscripten-core/emscripten/pull/7096 + random_device = function() { abort("random_device"); }; + } + FS.createDevice('/dev', 'random', random_device); + FS.createDevice('/dev', 'urandom', random_device); + // we're not going to emulate the actual shm device, + // just create the tmp dirs that reside in it commonly + FS.mkdir('/dev/shm'); + FS.mkdir('/dev/shm/tmp'); + },createSpecialDirectories:function() { + // create /proc/self/fd which allows /proc/self/fd/6 => readlink gives the name of the stream for fd 6 (see test_unistd_ttyname) + FS.mkdir('/proc'); + FS.mkdir('/proc/self'); + FS.mkdir('/proc/self/fd'); + FS.mount({ + mount: function() { + var node = FS.createNode('/proc/self', 'fd', 16384 | 511 /* 0777 */, 73); + node.node_ops = { + lookup: function(parent, name) { + var fd = +name; + var stream = FS.getStream(fd); + if (!stream) throw new FS.ErrnoError(8); + var ret = { + parent: null, + mount: { mountpoint: 'fake' }, + node_ops: { readlink: function() { return stream.path } } + }; + ret.parent = ret; // make it look like a simple root node + return ret; + } + }; + return node; + } + }, {}, '/proc/self/fd'); + },createStandardStreams:function() { + // TODO deprecate the old functionality of a single + // input / output callback and that utilizes FS.createDevice + // and instead require a unique set of stream ops + + // by default, we symlink the standard streams to the + // default tty devices. however, if the standard streams + // have been overwritten we create a unique device for + // them instead. + if (Module['stdin']) { + FS.createDevice('/dev', 'stdin', Module['stdin']); + } else { + FS.symlink('/dev/tty', '/dev/stdin'); + } + if (Module['stdout']) { + FS.createDevice('/dev', 'stdout', null, Module['stdout']); + } else { + FS.symlink('/dev/tty', '/dev/stdout'); + } + if (Module['stderr']) { + FS.createDevice('/dev', 'stderr', null, Module['stderr']); + } else { + FS.symlink('/dev/tty1', '/dev/stderr'); + } + + // open default streams for the stdin, stdout and stderr devices + var stdin = FS.open('/dev/stdin', 'r'); + var stdout = FS.open('/dev/stdout', 'w'); + var stderr = FS.open('/dev/stderr', 'w'); + },ensureErrnoError:function() { + if (FS.ErrnoError) return; + FS.ErrnoError = /** @this{Object} */ function ErrnoError(errno, node) { + this.node = node; + this.setErrno = /** @this{Object} */ function(errno) { + this.errno = errno; + }; + this.setErrno(errno); + this.message = 'FS error'; + + }; + FS.ErrnoError.prototype = new Error(); + FS.ErrnoError.prototype.constructor = FS.ErrnoError; + // Some errors may happen quite a bit, to avoid overhead we reuse them (and suffer a lack of stack info) + [44].forEach(function(code) { + FS.genericErrors[code] = new FS.ErrnoError(code); + FS.genericErrors[code].stack = ''; + }); + },staticInit:function() { + FS.ensureErrnoError(); + + FS.nameTable = new Array(4096); + + FS.mount(MEMFS, {}, '/'); + + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + + FS.filesystems = { + 'MEMFS': MEMFS, + }; + },init:function(input, output, error) { + FS.init.initialized = true; + + FS.ensureErrnoError(); + + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here + Module['stdin'] = input || Module['stdin']; + Module['stdout'] = output || Module['stdout']; + Module['stderr'] = error || Module['stderr']; + + FS.createStandardStreams(); + },quit:function() { + FS.init.initialized = false; + // force-flush all streams, so we get musl std streams printed out + var fflush = Module['_fflush']; + if (fflush) fflush(0); + // close all of our streams + for (var i = 0; i < FS.streams.length; i++) { + var stream = FS.streams[i]; + if (!stream) { + continue; + } + FS.close(stream); + } + },getMode:function(canRead, canWrite) { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode; + },joinPath:function(parts, forceRelative) { + var path = PATH.join.apply(null, parts); + if (forceRelative && path[0] == '/') path = path.substr(1); + return path; + },absolutePath:function(relative, base) { + return PATH_FS.resolve(base, relative); + },standardizePath:function(path) { + return PATH.normalize(path); + },findObject:function(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (ret.exists) { + return ret.object; + } else { + setErrNo(ret.error); + return null; + } + },analyzePath:function(path, dontResolveLastLink) { + // operate from within the context of the symlink's target + try { + var lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); + path = lookup.path; + } catch (e) { + } + var ret = { + isRoot: false, exists: false, error: 0, name: null, path: null, object: null, + parentExists: false, parentPath: null, parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { parent: true }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === '/'; + } catch (e) { + ret.error = e.errno; + }; + return ret; + },createFolder:function(parent, name, canRead, canWrite) { + var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); + var mode = FS.getMode(canRead, canWrite); + return FS.mkdir(path, mode); + },createPath:function(parent, path, canRead, canWrite) { + parent = typeof parent === 'string' ? parent : FS.getPath(parent); + var parts = path.split('/').reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current); + } catch (e) { + // ignore EEXIST + } + parent = current; + } + return current; + },createFile:function(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); + var mode = FS.getMode(canRead, canWrite); + return FS.create(path, mode); + },createDataFile:function(parent, name, data, canRead, canWrite, canOwn) { + var path = name ? PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name) : parent; + var mode = FS.getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + if (typeof data === 'string') { + var arr = new Array(data.length); + for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); + data = arr; + } + // make sure we can write to the file + FS.chmod(node, mode | 146); + var stream = FS.open(node, 'w'); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode); + } + return node; + },createDevice:function(parent, name, input, output) { + var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); + var mode = FS.getMode(!!input, !!output); + if (!FS.createDevice.major) FS.createDevice.major = 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + // Create a fake device that a set of stream ops to emulate + // the old behavior. + FS.registerDevice(dev, { + open: function(stream) { + stream.seekable = false; + }, + close: function(stream) { + // flush any pending line data + if (output && output.buffer && output.buffer.length) { + output(10); + } + }, + read: function(stream, buffer, offset, length, pos /* ignored */) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input(); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset+i] = result; + } + if (bytesRead) { + stream.node.timestamp = Date.now(); + } + return bytesRead; + }, + write: function(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset+i]); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + if (length) { + stream.node.timestamp = Date.now(); + } + return i; + } + }); + return FS.mkdev(path, mode, dev); + },createLink:function(parent, name, target, canRead, canWrite) { + var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); + return FS.symlink(target, path); + },forceLoadFile:function(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + var success = true; + if (typeof XMLHttpRequest !== 'undefined') { + throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else if (read_) { + // Command-line. + try { + // WARNING: Can't read binary files in V8's d8 or tracemonkey's js, as + // read() will try to parse UTF8. + obj.contents = intArrayFromString(read_(obj.url), true); + obj.usedBytes = obj.contents.length; + } catch (e) { + success = false; + } + } else { + throw new Error('Cannot load without read() or XMLHttpRequest.'); + } + if (!success) setErrNo(29); + return success; + },createLazyFile:function(parent, name, url, canRead, canWrite) { + // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse. + /** @constructor */ + function LazyUint8Array() { + this.lengthKnown = false; + this.chunks = []; // Loaded chunks. Index is the chunk number + } + LazyUint8Array.prototype.get = /** @this{Object} */ function LazyUint8Array_get(idx) { + if (idx > this.length-1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize)|0; + return this.getter(chunkNum)[chunkOffset]; + }; + LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { + this.getter = getter; + }; + LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { + // Find length + var xhr = new XMLHttpRequest(); + xhr.open('HEAD', url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + + var chunkSize = 1024*1024; // Chunk size in bytes + + if (!hasByteServing) chunkSize = datalength; + + // Function to get a range from the remote URL. + var doXHR = (function(from, to) { + if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); + + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + + // Some hints to the browser that we want binary data. + if (typeof Uint8Array != 'undefined') xhr.responseType = 'arraybuffer'; + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */(xhr.response || [])); + } else { + return intArrayFromString(xhr.responseText || '', true); + } + }); + var lazyArray = this; + lazyArray.setDataGetter(function(chunkNum) { + var start = chunkNum * chunkSize; + var end = (chunkNum+1) * chunkSize - 1; // including this byte + end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block + if (typeof(lazyArray.chunks[chunkNum]) === "undefined") { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof(lazyArray.chunks[chunkNum]) === "undefined") throw new Error("doXHR failed!"); + return lazyArray.chunks[chunkNum]; + }); + + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); + } + + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + }; + if (typeof XMLHttpRequest !== 'undefined') { + if (!ENVIRONMENT_IS_WORKER) throw 'Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'; + var lazyArray = new LazyUint8Array(); + Object.defineProperties(lazyArray, { + length: { + get: /** @this{Object} */ function() { + if(!this.lengthKnown) { + this.cacheLength(); + } + return this._length; + } + }, + chunkSize: { + get: /** @this{Object} */ function() { + if(!this.lengthKnown) { + this.cacheLength(); + } + return this._chunkSize; + } + } + }); + + var properties = { isDevice: false, contents: lazyArray }; + } else { + var properties = { isDevice: false, url: url }; + } + + var node = FS.createFile(parent, name, properties, canRead, canWrite); + // This is a total hack, but I want to get this lazy file code out of the + // core of MEMFS. If we want to keep this lazy file concept I feel it should + // be its own thin LAZYFS proxying calls to MEMFS. + if (properties.contents) { + node.contents = properties.contents; + } else if (properties.url) { + node.contents = null; + node.url = properties.url; + } + // Add a function that defers querying the file size until it is asked the first time. + Object.defineProperties(node, { + usedBytes: { + get: /** @this {FSNode} */ function() { return this.contents.length; } + } + }); + // override each stream op with one that tries to force load the lazy file first + var stream_ops = {}; + var keys = Object.keys(node.stream_ops); + keys.forEach(function(key) { + var fn = node.stream_ops[key]; + stream_ops[key] = function forceLoadLazyFile() { + if (!FS.forceLoadFile(node)) { + throw new FS.ErrnoError(29); + } + return fn.apply(null, arguments); + }; + }); + // use a custom read function + stream_ops.read = function stream_ops_read(stream, buffer, offset, length, position) { + if (!FS.forceLoadFile(node)) { + throw new FS.ErrnoError(29); + } + var contents = stream.node.contents; + if (position >= contents.length) + return 0; + var size = Math.min(contents.length - position, length); + if (contents.slice) { // normal array + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i]; + } + } else { + for (var i = 0; i < size; i++) { // LazyUint8Array from sync binary XHR + buffer[offset + i] = contents.get(position + i); + } + } + return size; + }; + node.stream_ops = stream_ops; + return node; + },createPreloadedFile:function(parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) { + Browser.init(); // XXX perhaps this method should move onto Browser? + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency('cp ' + fullname); // might have several active requests for the same fullname + function processData(byteArray) { + function finish(byteArray) { + if (preFinish) preFinish(); + if (!dontCreateFile) { + FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + if (onload) onload(); + removeRunDependency(dep); + } + var handled = false; + Module['preloadPlugins'].forEach(function(plugin) { + if (handled) return; + if (plugin['canHandle'](fullname)) { + plugin['handle'](byteArray, fullname, finish, function() { + if (onerror) onerror(); + removeRunDependency(dep); + }); + handled = true; + } + }); + if (!handled) finish(byteArray); + } + addRunDependency(dep); + if (typeof url == 'string') { + Browser.asyncLoad(url, function(byteArray) { + processData(byteArray); + }, onerror); + } else { + processData(url); + } + },indexedDB:function() { + return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + },DB_NAME:function() { + return 'EM_FS_' + window.location.pathname; + },DB_VERSION:20,DB_STORE_NAME:"FILE_DATA",saveFilesToDB:function(paths, onload, onerror) { + onload = onload || function(){}; + onerror = onerror || function(){}; + var indexedDB = FS.indexedDB(); + try { + var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); + } catch (e) { + return onerror(e); + } + openRequest.onupgradeneeded = function openRequest_onupgradeneeded() { + out('creating db'); + var db = openRequest.result; + db.createObjectStore(FS.DB_STORE_NAME); + }; + openRequest.onsuccess = function openRequest_onsuccess() { + var db = openRequest.result; + var transaction = db.transaction([FS.DB_STORE_NAME], 'readwrite'); + var files = transaction.objectStore(FS.DB_STORE_NAME); + var ok = 0, fail = 0, total = paths.length; + function finish() { + if (fail == 0) onload(); else onerror(); + } + paths.forEach(function(path) { + var putRequest = files.put(FS.analyzePath(path).object.contents, path); + putRequest.onsuccess = function putRequest_onsuccess() { ok++; if (ok + fail == total) finish() }; + putRequest.onerror = function putRequest_onerror() { fail++; if (ok + fail == total) finish() }; + }); + transaction.onerror = onerror; + }; + openRequest.onerror = onerror; + },loadFilesFromDB:function(paths, onload, onerror) { + onload = onload || function(){}; + onerror = onerror || function(){}; + var indexedDB = FS.indexedDB(); + try { + var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); + } catch (e) { + return onerror(e); + } + openRequest.onupgradeneeded = onerror; // no database to load from + openRequest.onsuccess = function openRequest_onsuccess() { + var db = openRequest.result; + try { + var transaction = db.transaction([FS.DB_STORE_NAME], 'readonly'); + } catch(e) { + onerror(e); + return; + } + var files = transaction.objectStore(FS.DB_STORE_NAME); + var ok = 0, fail = 0, total = paths.length; + function finish() { + if (fail == 0) onload(); else onerror(); + } + paths.forEach(function(path) { + var getRequest = files.get(path); + getRequest.onsuccess = function getRequest_onsuccess() { + if (FS.analyzePath(path).exists) { + FS.unlink(path); + } + FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true); + ok++; + if (ok + fail == total) finish(); + }; + getRequest.onerror = function getRequest_onerror() { fail++; if (ok + fail == total) finish() }; + }); + transaction.onerror = onerror; + }; + openRequest.onerror = onerror; + }};var SYSCALLS={mappings:{},DEFAULT_POLLMASK:5,umask:511,calculateAt:function(dirfd, path) { + if (path[0] !== '/') { + // relative path + var dir; + if (dirfd === -100) { + dir = FS.cwd(); + } else { + var dirstream = FS.getStream(dirfd); + if (!dirstream) throw new FS.ErrnoError(8); + dir = dirstream.path; + } + path = PATH.join2(dir, path); + } + return path; + },doStat:function(func, path, buf) { + try { + var stat = func(path); + } catch (e) { + if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { + // an error occurred while trying to look up the path; we should just report ENOTDIR + return -54; + } + throw e; + } + HEAP32[((buf)>>2)]=stat.dev; + HEAP32[(((buf)+(4))>>2)]=0; + HEAP32[(((buf)+(8))>>2)]=stat.ino; + HEAP32[(((buf)+(12))>>2)]=stat.mode; + HEAP32[(((buf)+(16))>>2)]=stat.nlink; + HEAP32[(((buf)+(20))>>2)]=stat.uid; + HEAP32[(((buf)+(24))>>2)]=stat.gid; + HEAP32[(((buf)+(28))>>2)]=stat.rdev; + HEAP32[(((buf)+(32))>>2)]=0; + (tempI64 = [stat.size>>>0,(tempDouble=stat.size,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[(((buf)+(40))>>2)]=tempI64[0],HEAP32[(((buf)+(44))>>2)]=tempI64[1]); + HEAP32[(((buf)+(48))>>2)]=4096; + HEAP32[(((buf)+(52))>>2)]=stat.blocks; + HEAP32[(((buf)+(56))>>2)]=(stat.atime.getTime() / 1000)|0; + HEAP32[(((buf)+(60))>>2)]=0; + HEAP32[(((buf)+(64))>>2)]=(stat.mtime.getTime() / 1000)|0; + HEAP32[(((buf)+(68))>>2)]=0; + HEAP32[(((buf)+(72))>>2)]=(stat.ctime.getTime() / 1000)|0; + HEAP32[(((buf)+(76))>>2)]=0; + (tempI64 = [stat.ino>>>0,(tempDouble=stat.ino,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[(((buf)+(80))>>2)]=tempI64[0],HEAP32[(((buf)+(84))>>2)]=tempI64[1]); + return 0; + },doMsync:function(addr, stream, len, flags, offset) { + var buffer = HEAPU8.slice(addr, addr + len); + FS.msync(stream, buffer, offset, len, flags); + },doMkdir:function(path, mode) { + // remove a trailing slash, if one - /a/b/ has basename of '', but + // we want to create b in the context of this function + path = PATH.normalize(path); + if (path[path.length-1] === '/') path = path.substr(0, path.length-1); + FS.mkdir(path, mode, 0); + return 0; + },doMknod:function(path, mode, dev) { + // we don't want this in the JS API as it uses mknod to create all nodes. + switch (mode & 61440) { + case 32768: + case 8192: + case 24576: + case 4096: + case 49152: + break; + default: return -28; + } + FS.mknod(path, mode, dev); + return 0; + },doReadlink:function(path, buf, bufsize) { + if (bufsize <= 0) return -28; + var ret = FS.readlink(path); + + var len = Math.min(bufsize, lengthBytesUTF8(ret)); + var endChar = HEAP8[buf+len]; + stringToUTF8(ret, buf, bufsize+1); + // readlink is one of the rare functions that write out a C string, but does never append a null to the output buffer(!) + // stringToUTF8() always appends a null byte, so restore the character under the null byte after the write. + HEAP8[buf+len] = endChar; + + return len; + },doAccess:function(path, amode) { + if (amode & ~7) { + // need a valid mode + return -28; + } + var node; + var lookup = FS.lookupPath(path, { follow: true }); + node = lookup.node; + if (!node) { + return -44; + } + var perms = ''; + if (amode & 4) perms += 'r'; + if (amode & 2) perms += 'w'; + if (amode & 1) perms += 'x'; + if (perms /* otherwise, they've just passed F_OK */ && FS.nodePermissions(node, perms)) { + return -2; + } + return 0; + },doDup:function(path, flags, suggestFD) { + var suggest = FS.getStream(suggestFD); + if (suggest) FS.close(suggest); + return FS.open(path, flags, 0, suggestFD, suggestFD).fd; + },doReadv:function(stream, iov, iovcnt, offset) { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAP32[(((iov)+(i*8))>>2)]; + var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; + var curr = FS.read(stream, HEAP8,ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) break; // nothing more to read + } + return ret; + },doWritev:function(stream, iov, iovcnt, offset) { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAP32[(((iov)+(i*8))>>2)]; + var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; + var curr = FS.write(stream, HEAP8,ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + } + return ret; + },varargs:undefined,get:function() { + SYSCALLS.varargs += 4; + var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; + return ret; + },getStr:function(ptr) { + var ret = UTF8ToString(ptr); + return ret; + },getStreamFromFD:function(fd) { + var stream = FS.getStream(fd); + if (!stream) throw new FS.ErrnoError(8); + return stream; + },get64:function(low, high) { + return low; + }};function _fd_close(fd) {try { + + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno; + } + } + + function _fd_read(fd, iov, iovcnt, pnum) {try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var num = SYSCALLS.doReadv(stream, iov, iovcnt); + HEAP32[((pnum)>>2)]=num + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno; + } + } + + function _fd_seek(fd, offset_low, offset_high, whence, newOffset) {try { + + + var stream = SYSCALLS.getStreamFromFD(fd); + var HIGH_OFFSET = 0x100000000; // 2^32 + // use an unsigned operator on low and shift high by 32-bits + var offset = offset_high * HIGH_OFFSET + (offset_low >>> 0); + + var DOUBLE_LIMIT = 0x20000000000000; // 2^53 + // we also check for equality since DOUBLE_LIMIT + 1 == DOUBLE_LIMIT + if (offset <= -DOUBLE_LIMIT || offset >= DOUBLE_LIMIT) { + return -61; + } + + FS.llseek(stream, offset, whence); + (tempI64 = [stream.position>>>0,(tempDouble=stream.position,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((newOffset)>>2)]=tempI64[0],HEAP32[(((newOffset)+(4))>>2)]=tempI64[1]); + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; // reset readdir state + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno; + } + } + + function _fd_write(fd, iov, iovcnt, pnum) {try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var num = SYSCALLS.doWritev(stream, iov, iovcnt); + HEAP32[((pnum)>>2)]=num + return 0; + } catch (e) { + if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno; + } + } + + + function _round(d) { + d = +d; + return d >= +0 ? +Math_floor(d + +0.5) : +Math_ceil(d - +0.5); + } + + function _setTempRet0($i) { + setTempRet0(($i) | 0); + } +var FSNode = /** @constructor */ function(parent, name, mode, rdev) { + if (!parent) { + parent = this; // root node sets parent to itself + } + this.parent = parent; + this.mount = parent.mount; + this.mounted = null; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.node_ops = {}; + this.stream_ops = {}; + this.rdev = rdev; + }; + var readMode = 292/*292*/ | 73/*73*/; + var writeMode = 146/*146*/; + Object.defineProperties(FSNode.prototype, { + read: { + get: /** @this{FSNode} */function() { + return (this.mode & readMode) === readMode; + }, + set: /** @this{FSNode} */function(val) { + val ? this.mode |= readMode : this.mode &= ~readMode; + } + }, + write: { + get: /** @this{FSNode} */function() { + return (this.mode & writeMode) === writeMode; + }, + set: /** @this{FSNode} */function(val) { + val ? this.mode |= writeMode : this.mode &= ~writeMode; + } + }, + isFolder: { + get: /** @this{FSNode} */function() { + return FS.isDir(this.mode); + } + }, + isDevice: { + get: /** @this{FSNode} */function() { + return FS.isChrdev(this.mode); + } + } + }); + FS.FSNode = FSNode; + FS.staticInit();; +var ASSERTIONS = false; + + + +/** @type {function(string, boolean=, number=)} */ +function intArrayFromString(stringy, dontAddNull, length) { + var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +} + +function intArrayToString(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + var chr = array[i]; + if (chr > 0xFF) { + if (ASSERTIONS) { + assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.'); + } + chr &= 0xFF; + } + ret.push(String.fromCharCode(chr)); + } + return ret.join(''); +} + + +// Copied from https://github.com/strophe/strophejs/blob/e06d027/src/polyfills.js#L149 + +// This code was written by Tyler Akins and has been placed in the +// public domain. It would be nice if you left this header intact. +// Base64 code from Tyler Akins -- http://rumkin.com + +/** + * Decodes a base64 string. + * @param {string} input The string to decode. + */ +var decodeBase64 = typeof atob === 'function' ? atob : function (input) { + var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + var output = ''; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 !== 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 !== 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + return output; +}; + +// Converts a string of base64 into a byte array. +// Throws error on invalid input. +function intArrayFromBase64(s) { + if (typeof ENVIRONMENT_IS_NODE === 'boolean' && ENVIRONMENT_IS_NODE) { + var buf; + try { + // TODO: Update Node.js externs, Closure does not recognize the following Buffer.from() + /**@suppress{checkTypes}*/ + buf = Buffer.from(s, 'base64'); + } catch (_) { + buf = new Buffer(s, 'base64'); + } + return new Uint8Array(buf['buffer'], buf['byteOffset'], buf['byteLength']); + } + + try { + var decoded = decodeBase64(s); + var bytes = new Uint8Array(decoded.length); + for (var i = 0 ; i < decoded.length ; ++i) { + bytes[i] = decoded.charCodeAt(i); + } + return bytes; + } catch (_) { + throw new Error('Converting base64 string to bytes failed.'); + } +} + +// If filename is a base64 data URI, parses and returns data (Buffer on node, +// Uint8Array otherwise). If filename is not a base64 data URI, returns undefined. +function tryParseAsDataURI(filename) { + if (!isDataURI(filename)) { + return; + } + + return intArrayFromBase64(filename.slice(dataURIPrefix.length)); +} + + +// ASM_LIBRARY EXTERN PRIMITIVES: Math_floor,Math_ceil + +var asmGlobalArg = {}; +var asmLibraryArg = { "emscripten_get_sbrk_ptr": _emscripten_get_sbrk_ptr, "emscripten_memcpy_big": _emscripten_memcpy_big, "emscripten_resize_heap": _emscripten_resize_heap, "fd_close": _fd_close, "fd_read": _fd_read, "fd_seek": _fd_seek, "fd_write": _fd_write, "getTempRet0": getTempRet0, "memory": wasmMemory, "round": _round, "setTempRet0": setTempRet0, "table": wasmTable }; +var asm = createWasm(); +/** @type {function(...*):?} */ +var ___wasm_call_ctors = Module["___wasm_call_ctors"] = function() { + return (___wasm_call_ctors = Module["___wasm_call_ctors"] = Module["asm"]["__wasm_call_ctors"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_new = Module["_FLAC__stream_decoder_new"] = function() { + return (_FLAC__stream_decoder_new = Module["_FLAC__stream_decoder_new"] = Module["asm"]["FLAC__stream_decoder_new"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_delete = Module["_FLAC__stream_decoder_delete"] = function() { + return (_FLAC__stream_decoder_delete = Module["_FLAC__stream_decoder_delete"] = Module["asm"]["FLAC__stream_decoder_delete"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_finish = Module["_FLAC__stream_decoder_finish"] = function() { + return (_FLAC__stream_decoder_finish = Module["_FLAC__stream_decoder_finish"] = Module["asm"]["FLAC__stream_decoder_finish"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_init_stream = Module["_FLAC__stream_decoder_init_stream"] = function() { + return (_FLAC__stream_decoder_init_stream = Module["_FLAC__stream_decoder_init_stream"] = Module["asm"]["FLAC__stream_decoder_init_stream"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_reset = Module["_FLAC__stream_decoder_reset"] = function() { + return (_FLAC__stream_decoder_reset = Module["_FLAC__stream_decoder_reset"] = Module["asm"]["FLAC__stream_decoder_reset"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_init_ogg_stream = Module["_FLAC__stream_decoder_init_ogg_stream"] = function() { + return (_FLAC__stream_decoder_init_ogg_stream = Module["_FLAC__stream_decoder_init_ogg_stream"] = Module["asm"]["FLAC__stream_decoder_init_ogg_stream"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_ogg_serial_number = Module["_FLAC__stream_decoder_set_ogg_serial_number"] = function() { + return (_FLAC__stream_decoder_set_ogg_serial_number = Module["_FLAC__stream_decoder_set_ogg_serial_number"] = Module["asm"]["FLAC__stream_decoder_set_ogg_serial_number"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_md5_checking = Module["_FLAC__stream_decoder_set_md5_checking"] = function() { + return (_FLAC__stream_decoder_set_md5_checking = Module["_FLAC__stream_decoder_set_md5_checking"] = Module["asm"]["FLAC__stream_decoder_set_md5_checking"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_respond = Module["_FLAC__stream_decoder_set_metadata_respond"] = function() { + return (_FLAC__stream_decoder_set_metadata_respond = Module["_FLAC__stream_decoder_set_metadata_respond"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_respond_application = Module["_FLAC__stream_decoder_set_metadata_respond_application"] = function() { + return (_FLAC__stream_decoder_set_metadata_respond_application = Module["_FLAC__stream_decoder_set_metadata_respond_application"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond_application"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_respond_all = Module["_FLAC__stream_decoder_set_metadata_respond_all"] = function() { + return (_FLAC__stream_decoder_set_metadata_respond_all = Module["_FLAC__stream_decoder_set_metadata_respond_all"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond_all"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_ignore = Module["_FLAC__stream_decoder_set_metadata_ignore"] = function() { + return (_FLAC__stream_decoder_set_metadata_ignore = Module["_FLAC__stream_decoder_set_metadata_ignore"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_ignore_application = Module["_FLAC__stream_decoder_set_metadata_ignore_application"] = function() { + return (_FLAC__stream_decoder_set_metadata_ignore_application = Module["_FLAC__stream_decoder_set_metadata_ignore_application"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore_application"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_set_metadata_ignore_all = Module["_FLAC__stream_decoder_set_metadata_ignore_all"] = function() { + return (_FLAC__stream_decoder_set_metadata_ignore_all = Module["_FLAC__stream_decoder_set_metadata_ignore_all"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore_all"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_get_state = Module["_FLAC__stream_decoder_get_state"] = function() { + return (_FLAC__stream_decoder_get_state = Module["_FLAC__stream_decoder_get_state"] = Module["asm"]["FLAC__stream_decoder_get_state"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_get_md5_checking = Module["_FLAC__stream_decoder_get_md5_checking"] = function() { + return (_FLAC__stream_decoder_get_md5_checking = Module["_FLAC__stream_decoder_get_md5_checking"] = Module["asm"]["FLAC__stream_decoder_get_md5_checking"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_process_single = Module["_FLAC__stream_decoder_process_single"] = function() { + return (_FLAC__stream_decoder_process_single = Module["_FLAC__stream_decoder_process_single"] = Module["asm"]["FLAC__stream_decoder_process_single"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_process_until_end_of_metadata = Module["_FLAC__stream_decoder_process_until_end_of_metadata"] = function() { + return (_FLAC__stream_decoder_process_until_end_of_metadata = Module["_FLAC__stream_decoder_process_until_end_of_metadata"] = Module["asm"]["FLAC__stream_decoder_process_until_end_of_metadata"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_decoder_process_until_end_of_stream = Module["_FLAC__stream_decoder_process_until_end_of_stream"] = function() { + return (_FLAC__stream_decoder_process_until_end_of_stream = Module["_FLAC__stream_decoder_process_until_end_of_stream"] = Module["asm"]["FLAC__stream_decoder_process_until_end_of_stream"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_new = Module["_FLAC__stream_encoder_new"] = function() { + return (_FLAC__stream_encoder_new = Module["_FLAC__stream_encoder_new"] = Module["asm"]["FLAC__stream_encoder_new"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_delete = Module["_FLAC__stream_encoder_delete"] = function() { + return (_FLAC__stream_encoder_delete = Module["_FLAC__stream_encoder_delete"] = Module["asm"]["FLAC__stream_encoder_delete"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_finish = Module["_FLAC__stream_encoder_finish"] = function() { + return (_FLAC__stream_encoder_finish = Module["_FLAC__stream_encoder_finish"] = Module["asm"]["FLAC__stream_encoder_finish"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_init_stream = Module["_FLAC__stream_encoder_init_stream"] = function() { + return (_FLAC__stream_encoder_init_stream = Module["_FLAC__stream_encoder_init_stream"] = Module["asm"]["FLAC__stream_encoder_init_stream"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_init_ogg_stream = Module["_FLAC__stream_encoder_init_ogg_stream"] = function() { + return (_FLAC__stream_encoder_init_ogg_stream = Module["_FLAC__stream_encoder_init_ogg_stream"] = Module["asm"]["FLAC__stream_encoder_init_ogg_stream"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_ogg_serial_number = Module["_FLAC__stream_encoder_set_ogg_serial_number"] = function() { + return (_FLAC__stream_encoder_set_ogg_serial_number = Module["_FLAC__stream_encoder_set_ogg_serial_number"] = Module["asm"]["FLAC__stream_encoder_set_ogg_serial_number"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_verify = Module["_FLAC__stream_encoder_set_verify"] = function() { + return (_FLAC__stream_encoder_set_verify = Module["_FLAC__stream_encoder_set_verify"] = Module["asm"]["FLAC__stream_encoder_set_verify"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_channels = Module["_FLAC__stream_encoder_set_channels"] = function() { + return (_FLAC__stream_encoder_set_channels = Module["_FLAC__stream_encoder_set_channels"] = Module["asm"]["FLAC__stream_encoder_set_channels"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_bits_per_sample = Module["_FLAC__stream_encoder_set_bits_per_sample"] = function() { + return (_FLAC__stream_encoder_set_bits_per_sample = Module["_FLAC__stream_encoder_set_bits_per_sample"] = Module["asm"]["FLAC__stream_encoder_set_bits_per_sample"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_sample_rate = Module["_FLAC__stream_encoder_set_sample_rate"] = function() { + return (_FLAC__stream_encoder_set_sample_rate = Module["_FLAC__stream_encoder_set_sample_rate"] = Module["asm"]["FLAC__stream_encoder_set_sample_rate"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_compression_level = Module["_FLAC__stream_encoder_set_compression_level"] = function() { + return (_FLAC__stream_encoder_set_compression_level = Module["_FLAC__stream_encoder_set_compression_level"] = Module["asm"]["FLAC__stream_encoder_set_compression_level"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_blocksize = Module["_FLAC__stream_encoder_set_blocksize"] = function() { + return (_FLAC__stream_encoder_set_blocksize = Module["_FLAC__stream_encoder_set_blocksize"] = Module["asm"]["FLAC__stream_encoder_set_blocksize"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_total_samples_estimate = Module["_FLAC__stream_encoder_set_total_samples_estimate"] = function() { + return (_FLAC__stream_encoder_set_total_samples_estimate = Module["_FLAC__stream_encoder_set_total_samples_estimate"] = Module["asm"]["FLAC__stream_encoder_set_total_samples_estimate"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_set_metadata = Module["_FLAC__stream_encoder_set_metadata"] = function() { + return (_FLAC__stream_encoder_set_metadata = Module["_FLAC__stream_encoder_set_metadata"] = Module["asm"]["FLAC__stream_encoder_set_metadata"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_get_state = Module["_FLAC__stream_encoder_get_state"] = function() { + return (_FLAC__stream_encoder_get_state = Module["_FLAC__stream_encoder_get_state"] = Module["asm"]["FLAC__stream_encoder_get_state"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_get_verify_decoder_state = Module["_FLAC__stream_encoder_get_verify_decoder_state"] = function() { + return (_FLAC__stream_encoder_get_verify_decoder_state = Module["_FLAC__stream_encoder_get_verify_decoder_state"] = Module["asm"]["FLAC__stream_encoder_get_verify_decoder_state"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_get_verify = Module["_FLAC__stream_encoder_get_verify"] = function() { + return (_FLAC__stream_encoder_get_verify = Module["_FLAC__stream_encoder_get_verify"] = Module["asm"]["FLAC__stream_encoder_get_verify"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_process = Module["_FLAC__stream_encoder_process"] = function() { + return (_FLAC__stream_encoder_process = Module["_FLAC__stream_encoder_process"] = Module["asm"]["FLAC__stream_encoder_process"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _FLAC__stream_encoder_process_interleaved = Module["_FLAC__stream_encoder_process_interleaved"] = function() { + return (_FLAC__stream_encoder_process_interleaved = Module["_FLAC__stream_encoder_process_interleaved"] = Module["asm"]["FLAC__stream_encoder_process_interleaved"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var ___errno_location = Module["___errno_location"] = function() { + return (___errno_location = Module["___errno_location"] = Module["asm"]["__errno_location"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var stackSave = Module["stackSave"] = function() { + return (stackSave = Module["stackSave"] = Module["asm"]["stackSave"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var stackRestore = Module["stackRestore"] = function() { + return (stackRestore = Module["stackRestore"] = Module["asm"]["stackRestore"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var stackAlloc = Module["stackAlloc"] = function() { + return (stackAlloc = Module["stackAlloc"] = Module["asm"]["stackAlloc"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _malloc = Module["_malloc"] = function() { + return (_malloc = Module["_malloc"] = Module["asm"]["malloc"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var _free = Module["_free"] = function() { + return (_free = Module["_free"] = Module["asm"]["free"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var __growWasmMemory = Module["__growWasmMemory"] = function() { + return (__growWasmMemory = Module["__growWasmMemory"] = Module["asm"]["__growWasmMemory"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_iii = Module["dynCall_iii"] = function() { + return (dynCall_iii = Module["dynCall_iii"] = Module["asm"]["dynCall_iii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_ii = Module["dynCall_ii"] = function() { + return (dynCall_ii = Module["dynCall_ii"] = Module["asm"]["dynCall_ii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_iiii = Module["dynCall_iiii"] = function() { + return (dynCall_iiii = Module["dynCall_iiii"] = Module["asm"]["dynCall_iiii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_jiji = Module["dynCall_jiji"] = function() { + return (dynCall_jiji = Module["dynCall_jiji"] = Module["asm"]["dynCall_jiji"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_viiiiii = Module["dynCall_viiiiii"] = function() { + return (dynCall_viiiiii = Module["dynCall_viiiiii"] = Module["asm"]["dynCall_viiiiii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_iiiii = Module["dynCall_iiiii"] = function() { + return (dynCall_iiiii = Module["dynCall_iiiii"] = Module["asm"]["dynCall_iiiii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_viiiiiii = Module["dynCall_viiiiiii"] = function() { + return (dynCall_viiiiiii = Module["dynCall_viiiiiii"] = Module["asm"]["dynCall_viiiiiii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_viiii = Module["dynCall_viiii"] = function() { + return (dynCall_viiii = Module["dynCall_viiii"] = Module["asm"]["dynCall_viiii"]).apply(null, arguments); +}; + +/** @type {function(...*):?} */ +var dynCall_viii = Module["dynCall_viii"] = function() { + return (dynCall_viii = Module["dynCall_viii"] = Module["asm"]["dynCall_viii"]).apply(null, arguments); +}; + + + + + +// === Auto-generated postamble setup entry stuff === + + + + +Module["ccall"] = ccall; +Module["cwrap"] = cwrap; +Module["setValue"] = setValue; +Module["getValue"] = getValue; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +var calledRun; + +/** + * @constructor + * @this {ExitStatus} + */ +function ExitStatus(status) { + this.name = "ExitStatus"; + this.message = "Program terminated with exit(" + status + ")"; + this.status = status; +} + +var calledMain = false; + + +dependenciesFulfilled = function runCaller() { + // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) + if (!calledRun) run(); + if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled +}; + + + + + +/** @type {function(Array=)} */ +function run(args) { + args = args || arguments_; + + if (runDependencies > 0) { + return; + } + + + preRun(); + + if (runDependencies > 0) return; // a preRun added a dependency, run will be called later + + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + if (calledRun) return; + calledRun = true; + Module['calledRun'] = true; + + if (ABORT) return; + + initRuntime(); + + preMain(); + + if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); + + + postRun(); + } + + if (Module['setStatus']) { + Module['setStatus']('Running...'); + setTimeout(function() { + setTimeout(function() { + Module['setStatus'](''); + }, 1); + doRun(); + }, 1); + } else + { + doRun(); + } +} +Module['run'] = run; + + +/** @param {boolean|number=} implicit */ +function exit(status, implicit) { + + // if this is just main exit-ing implicitly, and the status is 0, then we + // don't need to do anything here and can just leave. if the status is + // non-zero, though, then we need to report it. + // (we may have warned about this earlier, if a situation justifies doing so) + if (implicit && noExitRuntime && status === 0) { + return; + } + + if (noExitRuntime) { + } else { + + ABORT = true; + EXITSTATUS = status; + + exitRuntime(); + + if (Module['onExit']) Module['onExit'](status); + } + + quit_(status, new ExitStatus(status)); +} + +if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].pop()(); + } +} + + + noExitRuntime = true; + +run(); + + + + + + +// {{MODULE_ADDITIONS}} + + + +//libflac function wrappers + +/** + * HELPER read/extract stream info meta-data from frame header / meta-data + * @param {POINTER} p_streaminfo + * @returns StreamInfo + */ +function _readStreamInfo(p_streaminfo){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_STREAMINFO (0) + + /* + typedef struct { + unsigned min_blocksize, max_blocksize; + unsigned min_framesize, max_framesize; + unsigned sample_rate; + unsigned channels; + unsigned bits_per_sample; + FLAC__uint64 total_samples; + FLAC__byte md5sum[16]; + } FLAC__StreamMetadata_StreamInfo; + */ + + var min_blocksize = Module.getValue(p_streaminfo,'i32');//4 bytes + var max_blocksize = Module.getValue(p_streaminfo+4,'i32');//4 bytes + + var min_framesize = Module.getValue(p_streaminfo+8,'i32');//4 bytes + var max_framesize = Module.getValue(p_streaminfo+12,'i32');//4 bytes + + var sample_rate = Module.getValue(p_streaminfo+16,'i32');//4 bytes + var channels = Module.getValue(p_streaminfo+20,'i32');//4 bytes + + var bits_per_sample = Module.getValue(p_streaminfo+24,'i32');//4 bytes + + //FIXME should be at p_streaminfo+28, but seems to be at p_streaminfo+32 + var total_samples = Module.getValue(p_streaminfo+32,'i64');//8 bytes + + var md5sum = _readMd5(p_streaminfo+40);//16 bytes + + return { + min_blocksize: min_blocksize, + max_blocksize: max_blocksize, + min_framesize: min_framesize, + max_framesize: max_framesize, + sampleRate: sample_rate, + channels: channels, + bitsPerSample: bits_per_sample, + total_samples: total_samples, + md5sum: md5sum + }; +} + +/** + * read MD5 checksum + * @param {POINTER} p_md5 + * @returns {String} as HEX string representation + */ +function _readMd5(p_md5){ + + var sb = [], v, str; + for(var i=0, len = 16; i < len; ++i){ + v = Module.getValue(p_md5+i,'i8');//1 byte + if(v < 0) v = 256 + v;//<- "convert" to uint8, if necessary + str = v.toString(16); + if(str.length < 2) str = '0' + str;//<- add padding, if necessary + sb.push(str); + } + return sb.join(''); +} + +/** + * HELPER: read frame data + * + * @param {POINTER} p_frame + * @param {CodingOptions} [enc_opt] + * @returns FrameHeader + */ +function _readFrameHdr(p_frame, enc_opt){ + + /* + typedef struct { + unsigned blocksize; + unsigned sample_rate; + unsigned channels; + FLAC__ChannelAssignment channel_assignment; + unsigned bits_per_sample; + FLAC__FrameNumberType number_type; + union { + FLAC__uint32 frame_number; + FLAC__uint64 sample_number; + } number; + FLAC__uint8 crc; + } FLAC__FrameHeader; + */ + + var blocksize = Module.getValue(p_frame,'i32');//4 bytes + var sample_rate = Module.getValue(p_frame+4,'i32');//4 bytes + var channels = Module.getValue(p_frame+8,'i32');//4 bytes + + // 0: FLAC__CHANNEL_ASSIGNMENT_INDEPENDENT independent channels + // 1: FLAC__CHANNEL_ASSIGNMENT_LEFT_SIDE left+side stereo + // 2: FLAC__CHANNEL_ASSIGNMENT_RIGHT_SIDE right+side stereo + // 3: FLAC__CHANNEL_ASSIGNMENT_MID_SIDE mid+side stereo + var channel_assignment = Module.getValue(p_frame+12,'i32');//4 bytes + + var bits_per_sample = Module.getValue(p_frame+16,'i32'); + + // 0: FLAC__FRAME_NUMBER_TYPE_FRAME_NUMBER number contains the frame number + // 1: FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER number contains the sample number of first sample in frame + var number_type = Module.getValue(p_frame+20,'i32'); + + // union {} number: The frame number or sample number of first sample in frame; use the number_type value to determine which to use. + var frame_number = Module.getValue(p_frame+24,'i32'); + var sample_number = Module.getValue(p_frame+24,'i64'); + + var number = number_type === 0? frame_number : sample_number; + var numberType = number_type === 0? 'frames' : 'samples'; + + var crc = Module.getValue(p_frame+36,'i8'); + + var subframes; + if(enc_opt && enc_opt.analyseSubframes){ + var subOffset = {offset: 40}; + subframes = []; + for(var i=0; i < channels; ++i){ + subframes.push(_readSubFrameHdr(p_frame, subOffset, blocksize, enc_opt)); + } + //TODO read footer + // console.log(' footer crc ', Module.getValue(p_frame + subOffset.offset,'i16')); + } + + return { + blocksize: blocksize, + sampleRate: sample_rate, + channels: channels, + channelAssignment: channel_assignment, + bitsPerSample: bits_per_sample, + number: number, + numberType: numberType, + crc: crc, + subframes: subframes + }; +} + + +function _readSubFrameHdr(p_subframe, subOffset, block_size, enc_opt){ + /* + FLAC__SubframeType type + union { + FLAC__Subframe_Constant constant + FLAC__Subframe_Fixed fixed + FLAC__Subframe_LPC lpc + FLAC__Subframe_Verbatim verbatim + } data + unsigned wasted_bits + */ + + var type = Module.getValue(p_subframe + subOffset.offset, 'i32'); + subOffset.offset += 4; + + var data; + switch(type){ + case 0: //FLAC__SUBFRAME_TYPE_CONSTANT + data = {value: Module.getValue(p_subframe + subOffset.offset, 'i32')}; + subOffset.offset += 284;//4; + break; + case 1: //FLAC__SUBFRAME_TYPE_VERBATIM + data = Module.getValue(p_subframe + subOffset.offset, 'i32'); + subOffset.offset += 284;//4; + break; + case 2: //FLAC__SUBFRAME_TYPE_FIXED + data = _readSubFrameHdrFixedData(p_subframe, subOffset, block_size, false, enc_opt); + break; + case 3: //FLAC__SUBFRAME_TYPE_LPC + data = _readSubFrameHdrFixedData(p_subframe, subOffset, block_size, true, enc_opt); + break; + } + + var offset = subOffset.offset; + var wasted_bits = Module.getValue(p_subframe + offset, 'i32'); + subOffset.offset += 4; + + return { + type: type,//['CONSTANT', 'VERBATIM', 'FIXED', 'LPC'][type], + data: data, + wastedBits: wasted_bits + } +} + +function _readSubFrameHdrFixedData(p_subframe_data, subOffset, block_size, is_lpc, enc_opt){ + + var offset = subOffset.offset; + + var data = {order: -1, contents: {parameters: [], rawBits: []}}; + //FLAC__Subframe_Fixed: + // FLAC__EntropyCodingMethod entropy_coding_method + // unsigned order + // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER] + // const FLAC__int32 * residual + + //FLAC__EntropyCodingMethod: + // FLAC__EntropyCodingMethodType type + // union { + // FLAC__EntropyCodingMethod_PartitionedRice partitioned_rice + // } data + + //FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE 0 Residual is coded by partitioning into contexts, each with it's own 4-bit Rice parameter. + //FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2 1 Residual is coded by partitioning into contexts, each with it's own 5-bit Rice parameter. + var entropyType = Module.getValue(p_subframe_data, 'i32'); + offset += 4; + + //FLAC__EntropyCodingMethod_PartitionedRice: + // unsigned order + var entropyOrder = Module.getValue(p_subframe_data + offset, 'i32'); + data.order = entropyOrder; + offset += 4; + + //FLAC__EntropyCodingMethod_PartitionedRice: + // FLAC__EntropyCodingMethod_PartitionedRiceContents * contents + var partitions = 1 << entropyOrder, params = data.contents.parameters, raws = data.contents.rawBits; + //FLAC__EntropyCodingMethod_PartitionedRiceContents + // unsigned * parameters + // unsigned * raw_bits + // unsigned capacity_by_order + var ppart = Module.getValue(p_subframe_data + offset, 'i32'); + var pparams = Module.getValue(ppart, 'i32'); + var praw = Module.getValue(ppart + 4, 'i32'); + data.contents.capacityByOrder = Module.getValue(ppart + 8, 'i32'); + for(var i=0; i < partitions; ++i){ + params.push(Module.getValue(pparams + (i*4), 'i32')); + raws.push(Module.getValue(praw + (i*4), 'i32')); + } + offset += 4; + + //FLAC__Subframe_Fixed: + // unsigned order + var order = Module.getValue(p_subframe_data + offset, 'i32'); + offset += 4; + + var warmup = [], res; + + if(is_lpc){ + //FLAC__Subframe_LPC + + // unsigned qlp_coeff_precision + var qlp_coeff_precision = Module.getValue(p_subframe_data + offset, 'i32'); + offset += 4; + // int quantization_level + var quantization_level = Module.getValue(p_subframe_data + offset, 'i32'); + offset += 4; + + //FLAC__Subframe_LPC : + // FLAC__int32 qlp_coeff [FLAC__MAX_LPC_ORDER] + var qlp_coeff = []; + for(var i=0; i < order; ++i){ + qlp_coeff.push(Module.getValue(p_subframe_data + offset, 'i32')); + offset += 4; + } + data.qlp_coeff = qlp_coeff; + data.qlp_coeff_precision = qlp_coeff_precision; + data.quantization_level = quantization_level; + + //FLAC__Subframe_LPC: + // FLAC__int32 warmup [FLAC__MAX_LPC_ORDER] + offset = subOffset.offset + 152; + offset = _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order); + + //FLAC__Subframe_LPC: + // const FLAC__int32 * residual + if(enc_opt && enc_opt.analyseResiduals){ + offset = subOffset.offset + 280; + res = _readSubFrameHdrResidual(p_subframe_data + offset, block_size, order); + } + + } else { + + //FLAC__Subframe_Fixed: + // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER] + offset = _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order); + + //FLAC__Subframe_Fixed: + // const FLAC__int32 * residual + offset = subOffset.offset + 32; + if(enc_opt && enc_opt.analyseResiduals){ + res = _readSubFrameHdrResidual(p_subframe_data + offset, block_size, order); + } + } + + subOffset.offset += 284; + return { + partition: { + type: entropyType, + data: data + }, + order: order, + warmup: warmup, + residual: res + } +} + + +function _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order){ + + // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER | FLAC__MAX_LPC_ORDER] + for(var i=0; i < order; ++i){ + warmup.push(Module.getValue(p_subframe_data + offset, 'i32')); + offset += 4; + } + return offset; +} + + +function _readSubFrameHdrResidual(p_subframe_data_res, block_size, order){ + // const FLAC__int32 * residual + var pres = Module.getValue(p_subframe_data_res, 'i32'); + var res = [];//Module.getValue(pres, 'i32'); + //TODO read residual all values(?) + // -> "The residual signal, length == (blocksize minus order) samples. + for(var i=0, size = block_size - order; i < size; ++i){ + res.push(Module.getValue(pres + (i*4), 'i32')); + } + return res; +} + +function _readConstChar(ptr, length, sb){ + sb.splice(0); + var ch; + for(var i=0; i < length; ++i){ + ch = Module.getValue(ptr + i,'i8'); + if(ch === 0){ + break; + } + sb.push(String.fromCodePoint(ch)); + } + return sb.join(''); +} + +function _readNullTerminatedChar(ptr, sb){ + sb.splice(0); + var ch = 1, i = 0; + while(ch > 0){ + ch = Module.getValue(ptr + i++, 'i8'); + if(ch === 0){ + break; + } + sb.push(String.fromCodePoint(ch)); + } + return sb.join(''); +} + + +/** + * HELPER read/extract padding metadata meta-data from meta-data block + * @param {POINTER} p_padding_metadata + * @returns PaddingMetadata + */ +function _readPaddingMetadata(p_padding_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_PADDING (1) + + //FLAC__StreamMetadata_Padding: + // int dummy + return { + dummy: Module.getValue(p_padding_metadata,'i32') + } +} + +/** + * HELPER read/extract application metadata meta-data from meta-data block + * @param {POINTER} p_application_metadata + * @returns ApplicationMetadata + */ +function _readApplicationMetadata(p_application_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_APPLICATION (2) + + //FLAC__StreamMetadata_Application: + // FLAC__byte id [4] + // FLAC__byte * data + return { + id : Module.getValue(p_application_metadata,'i32'), + data: Module.getValue(p_application_metadata + 4,'i32')//TODO should read (binary) data? + } +} + + +/** + * HELPER read/extract seek table metadata meta-data from meta-data block + * @param {POINTER} p_seek_table_metadata + * @returns SeekTableMetadata + */ +function _readSeekTableMetadata(p_seek_table_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_SEEKTABLE (3) + + //FLAC__StreamMetadata_SeekTable: + // unsigned num_points + // FLAC__StreamMetadata_SeekPoint * points + + var num_points = Module.getValue(p_seek_table_metadata,'i32'); + + var ptrPoints = Module.getValue(p_seek_table_metadata + 4,'i32'); + var points = []; + for(var i=0; i < num_points; ++i){ + + //FLAC__StreamMetadata_SeekPoint: + // FLAC__uint64 sample_number + // FLAC__uint64 stream_offset + // unsigned frame_samples + + points.push({ + sample_number: Module.getValue(ptrPoints + (i * 24),'i64'), + stream_offset: Module.getValue(ptrPoints + (i * 24) + 8,'i64'), + frame_samples: Module.getValue(ptrPoints + (i * 24) + 16,'i32') + }); + } + + return { + num_points: num_points, + points: points + } +} + +/** + * HELPER read/extract vorbis comment meta-data from meta-data block + * @param {POINTER} p_vorbiscomment + * @returns VorbisComment + */ +function _readVorbisComment(p_vorbiscomment){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_VORBIS_COMMENT (4) + + // FLAC__StreamMetadata_VorbisComment + // FLAC__StreamMetadata_VorbisComment_Entry vendor_string: + // FLAC__uint32 length + // FLAC__byte * entry + var length = Module.getValue(p_vorbiscomment,'i32'); + var entry = Module.getValue(p_vorbiscomment + 4,'i32'); + + var sb = []; + var strEntry = _readConstChar(entry, length, sb); + + // FLAC__uint32 num_comments + var num_comments = Module.getValue(p_vorbiscomment + 8,'i32'); + + // FLAC__StreamMetadata_VorbisComment_Entry * comments + var comments = [], clen, centry; + var pc = Module.getValue(p_vorbiscomment + 12, 'i32') + for(var i=0; i < num_comments; ++i){ + + // FLAC__StreamMetadata_VorbisComment_Entry + // FLAC__uint32 length + // FLAC__byte * entry + + clen = Module.getValue(pc + (i*8), 'i32'); + if(clen === 0){ + continue; + } + + centry = Module.getValue(pc + (i*8) + 4, 'i32'); + comments.push(_readConstChar(centry, clen, sb)); + } + + return { + vendor_string: strEntry, + num_comments: num_comments, + comments: comments + } +} + +/** + * HELPER read/extract cue sheet meta-data from meta-data block + * @param {POINTER} p_cue_sheet + * @returns CueSheetMetadata + */ +function _readCueSheetMetadata(p_cue_sheet){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_CUESHEET (5) + + // char media_catalog_number [129] + // FLAC__uint64 lead_in + // FLAC__bool is_cd + // unsigned num_tracks + // FLAC__StreamMetadata_CueSheet_Track * tracks + + var sb = []; + var media_catalog_number = _readConstChar(p_cue_sheet, 129, sb); + + var lead_in = Module.getValue(p_cue_sheet + 136,'i64'); + + var is_cd = Module.getValue(p_cue_sheet + 144,'i8'); + var num_tracks = Module.getValue(p_cue_sheet + 148,'i32'); + + var ptrTrack = Module.getValue(p_cue_sheet + 152,'i32'); + var tracks = [], trackOffset = ptrTrack; + if(ptrTrack !== 0){ + + for(var i=0; i < num_tracks; ++i){ + + var tr = _readCueSheetMetadata_track(trackOffset, sb); + tracks.push(tr); + trackOffset += 32; + } + } + + return { + media_catalog_number: media_catalog_number, + lead_in: lead_in, + is_cd: is_cd, + num_tracks: num_tracks, + tracks: tracks + } +} + +/** + * helper read track data for cue-sheet metadata + * @param {POINTER} p_cue_sheet_track pointer to the track data + * @param {string[]} sb "string buffer" temporary buffer for reading string (may be reset) + * @return {CueSheetTrack} + */ +function _readCueSheetMetadata_track(p_cue_sheet_track, sb){ + + // FLAC__StreamMetadata_CueSheet_Track: + // FLAC__uint64 offset + // FLAC__byte number + // char isrc [13] + // unsigned type:1 + // unsigned pre_emphasis:1 + // FLAC__byte num_indices + // FLAC__StreamMetadata_CueSheet_Index * indices + + var typePremph = Module.getValue(p_cue_sheet_track + 22,'i8'); + var num_indices = Module.getValue(p_cue_sheet_track + 23,'i8'); + + var indices = []; + var track = { + offset: Module.getValue(p_cue_sheet_track,'i64'), + number: Module.getValue(p_cue_sheet_track + 8,'i8') &255, + isrc: _readConstChar(p_cue_sheet_track + 9, 13, sb), + type: typePremph & 1? 'NON_AUDIO' : 'AUDIO', + pre_emphasis: !!(typePremph & 2), + num_indices: num_indices, + indices: indices + } + + var idx; + if(num_indices > 0){ + idx = Module.getValue(p_cue_sheet_track + 24,'i32'); + + //FLAC__StreamMetadata_CueSheet_Index: + // FLAC__uint64 offset + // FLAC__byte number + + for(var i=0; i < num_indices; ++i){ + indices.push({ + offset: Module.getValue(idx + (i*16),'i64'), + number: Module.getValue(idx + (i*16) + 8,'i8') + }); + } + } + + return track; +} + +/** + * HELPER read/extract picture meta-data from meta-data block + * @param {POINTER} p_picture_metadata + * @returns PictureMetadata + */ +function _readPictureMetadata(p_picture_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_PICTURE (6) + + // FLAC__StreamMetadata_Picture_Type type + // char * mime_type + // FLAC__byte * description + // FLAC__uint32 width + // FLAC__uint32 height + // FLAC__uint32 depth + // FLAC__uint32 colors + // FLAC__uint32 data_length + // FLAC__byte * data + + var type = Module.getValue(p_picture_metadata,'i32'); + + var mime = Module.getValue(p_picture_metadata + 4,'i32'); + + var sb = []; + var mime_type = _readNullTerminatedChar(mime, sb); + + var desc = Module.getValue(p_picture_metadata + 8,'i32'); + var description = _readNullTerminatedChar(desc, sb); + + var width = Module.getValue(p_picture_metadata + 12,'i32'); + var height = Module.getValue(p_picture_metadata + 16,'i32'); + var depth = Module.getValue(p_picture_metadata + 20,'i32'); + var colors = Module.getValue(p_picture_metadata + 24,'i32'); + var data_length = Module.getValue(p_picture_metadata + 28,'i32'); + + var data = Module.getValue(p_picture_metadata + 32,'i32'); + + var buffer = Uint8Array.from(Module.HEAPU8.subarray(data, data + data_length)); + + return { + type: type, + mime_type: mime_type, + description: description, + width: width, + height: height, + depth: depth, + colors: colors, + data_length: data_length, + data: buffer + } +} + +/** + * HELPER workaround / fix for returned write-buffer when decoding FLAC + * + * @param {number} heapOffset + * the offset for the data on HEAPU8 + * @param {Uint8Array} newBuffer + * the target buffer into which the data should be written -- with the correct (block) size + * @param {boolean} applyFix + * whether or not to apply the data repair heuristics + * (handling duplicated/triplicated values in raw data) + */ +function __fix_write_buffer(heapOffset, newBuffer, applyFix){ + + var dv = new DataView(newBuffer.buffer); + var targetSize = newBuffer.length; + + var increase = !applyFix? 1 : 2;//<- for FIX/workaround, NOTE: e.g. if 24-bit padding occurres, there is no fix/increase needed (more details comment below) + var buffer = HEAPU8.subarray(heapOffset, heapOffset + targetSize * increase); + + // FIXME for some reason, the bytes values 0 (min) and 255 (max) get "triplicated", + // or inserted "doubled" which should be ignored, i.e. + // x x x -> x + // x x -> + // where x is 0 or 255 + // -> HACK for now: remove/"over-read" 2 of the values, for each of these triplets/doublications + var jump, isPrint; + for(var i=0, j=0, size = buffer.length; i < size && j < targetSize; ++i, ++j){ + + if(i === size-1 && j < targetSize - 1){ + //increase heap-view, in order to read more (valid) data into the target buffer + buffer = HEAPU8.subarray(heapOffset, size + targetSize); + size = buffer.length; + } + + // NOTE if e.g. 24-bit padding occurres, there does not seem to be no duplication/triplication of 255 or 0, so must not try to fix! + if(applyFix && (buffer[i] === 0 || buffer[i] === 255)){ + + jump = 0; + isPrint = true; + + if(i + 1 < size && buffer[i] === buffer[i+1]){ + + ++jump; + + if(i + 2 < size){ + if(buffer[i] === buffer[i+2]){ + ++jump; + } else { + //if only 2 occurrences: ignore value + isPrint = false; + } + } + }//else: if single value: do print (an do not jump) + + + if(isPrint){ + dv.setUint8(j, buffer[i]); + if(jump === 2 && i + 3 < size && buffer[i] === buffer[i+3]){ + //special case for reducing triples in case the following value is also the same + // (ie. something like: x x x |+ x) + // -> then: do write the value one more time, and jump one further ahead + // i.e. if value occurs 4 times in a row, write 2 values + ++jump; + dv.setUint8(++j, buffer[i]); + } + } else { + --j; + } + + i += jump;//<- apply jump, if there were value duplications + + } else { + dv.setUint8(j, buffer[i]); + } + + } +} + + +// FLAC__STREAM_DECODER_READ_STATUS_CONTINUE The read was OK and decoding can continue. +// FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM The read was attempted while at the end of the stream. Note that the client must only return this value when the read callback was called when already at the end of the stream. Otherwise, if the read itself moves to the end of the stream, the client should still return the data and FLAC__STREAM_DECODER_READ_STATUS_CONTINUE, and then on the next read callback it should return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM with a byte count of 0. +// FLAC__STREAM_DECODER_READ_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. +var FLAC__STREAM_DECODER_READ_STATUS_CONTINUE = 0; +var FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM = 1; +var FLAC__STREAM_DECODER_READ_STATUS_ABORT = 2; + +// FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE The write was OK and decoding can continue. +// FLAC__STREAM_DECODER_WRITE_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. +var FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE = 0; +var FLAC__STREAM_DECODER_WRITE_STATUS_ABORT = 1; + +/** + * @interface FLAC__StreamDecoderInitStatus + * @memberOf Flac + * + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_OK"} 0 Initialization was successful. + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_UNSUPPORTED_CONTAINER"} 1 The library was not compiled with support for the given container format. + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS"} 2 A required callback was not supplied. + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_MEMORY_ALLOCATION_ERROR"} 3 An error occurred allocating memory. + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_ERROR_OPENING_FILE"} 4 fopen() failed in FLAC__stream_decoder_init_file() or FLAC__stream_decoder_init_ogg_file(). + * @property {"FLAC__STREAM_DECODER_INIT_STATUS_ALREADY_INITIALIZED"} 5 FLAC__stream_decoder_init_*() was called when the decoder was already initialized, usually because FLAC__stream_decoder_finish() was not called. + */ +var FLAC__STREAM_DECODER_INIT_STATUS_OK = 0; +var FLAC__STREAM_DECODER_INIT_STATUS_UNSUPPORTED_CONTAINER = 1; +var FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS = 2; +var FLAC__STREAM_DECODER_INIT_STATUS_MEMORY_ALLOCATION_ERROR = 3; +var FLAC__STREAM_DECODER_INIT_STATUS_ERROR_OPENING_FILE = 4; +var FLAC__STREAM_DECODER_INIT_STATUS_ALREADY_INITIALIZED = 5; + +/** + * @interface FLAC__StreamEncoderInitStatus + * @memberOf Flac + * + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_OK"} 0 Initialization was successful. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_ENCODER_ERROR"} 1 General failure to set up encoder; call FLAC__stream_encoder_get_state() for cause. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_UNSUPPORTED_CONTAINER"} 2 The library was not compiled with support for the given container format. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS"} 3 A required callback was not supplied. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_NUMBER_OF_CHANNELS"} 4 The encoder has an invalid setting for number of channels. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BITS_PER_SAMPLE"} 5 The encoder has an invalid setting for bits-per-sample. FLAC supports 4-32 bps but the reference encoder currently supports only up to 24 bps. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE"} 6 The encoder has an invalid setting for the input sample rate. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE"} 7 The encoder has an invalid setting for the block size. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_MAX_LPC_ORDER"} 8 The encoder has an invalid setting for the maximum LPC order. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_QLP_COEFF_PRECISION"} 9 The encoder has an invalid setting for the precision of the quantized linear predictor coefficients. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_BLOCK_SIZE_TOO_SMALL_FOR_LPC_ORDER"} 10 The specified block size is less than the maximum LPC order. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE"} 11 The encoder is bound to the Subset but other settings violate it. + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_METADATA"} 12 The metadata input to the encoder is invalid, in one of the following ways: + * FLAC__stream_encoder_set_metadata() was called with a null pointer but a block count > 0 + * One of the metadata blocks contains an undefined type + * It contains an illegal CUESHEET as checked by FLAC__format_cuesheet_is_legal() + * It contains an illegal SEEKTABLE as checked by FLAC__format_seektable_is_legal() + * It contains more than one SEEKTABLE block or more than one VORBIS_COMMENT block + * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_ALREADY_INITIALIZED"} 13 FLAC__stream_encoder_init_*() was called when the encoder was already initialized, usually because FLAC__stream_encoder_finish() was not called. + */ +var FLAC__STREAM_ENCODER_INIT_STATUS_OK = 0; +var FLAC__STREAM_ENCODER_INIT_STATUS_ENCODER_ERROR = 1; +var FLAC__STREAM_ENCODER_INIT_STATUS_UNSUPPORTED_CONTAINER = 2; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS = 3; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_NUMBER_OF_CHANNELS = 4; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BITS_PER_SAMPLE = 5; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE = 6; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE = 7; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_MAX_LPC_ORDER = 8; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_QLP_COEFF_PRECISION = 9; +var FLAC__STREAM_ENCODER_INIT_STATUS_BLOCK_SIZE_TOO_SMALL_FOR_LPC_ORDER = 10; +var FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE = 11; +var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_METADATA = 12; +var FLAC__STREAM_ENCODER_INIT_STATUS_ALREADY_INITIALIZED = 13; + +//FLAC__STREAM_ENCODER_WRITE_STATUS_OK The write was OK and encoding can continue. +//FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR An unrecoverable error occurred. The encoder will return from the process call +var FLAC__STREAM_ENCODER_WRITE_STATUS_OK = 0; +var FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR = 1; + + +/** + * Map for encoder/decoder callback functions + * + *
[ID] -> {function_type: FUNCTION}
+ * + * type: {[id: number]: {[callback_type: string]: function}} + * @private + */ +var coders = {}; + +/** + * Get a registered callback for the encoder / decoder instance + * + * @param {Number} p_coder + * the encoder/decoder pointer (ID) + * @param {String} func_type + * the callback type, one of + * "write" | "read" | "error" | "metadata" + * @returns {Function} the callback (or VOID if there is no callback registered) + * @private + */ +function getCallback(p_coder, func_type){ + if(coders[p_coder]){ + return coders[p_coder][func_type]; + } +} + +/** + * Register a callback for an encoder / decoder instance (will / should be deleted, when finish()/delete()) + * + * @param {Number} p_coder + * the encoder/decoder pointer (ID) + * @param {String} func_type + * the callback type, one of + * "write" | "read" | "error" | "metadata" + * @param {Function} callback + * the callback function + * @private + */ +function setCallback(p_coder, func_type, callback){ + if(!coders[p_coder]){ + coders[p_coder] = {}; + } + coders[p_coder][func_type] = callback; +} + +/** + * Get coding options for the encoder / decoder instance: + * returns FALSY when not set. + * + * @param {Number} p_coder + * the encoder/decoder pointer (ID) + * @returns {CodingOptions} the coding options + * @private + * @memberOf Flac + */ +function _getOptions(p_coder){ + if(coders[p_coder]){ + return coders[p_coder]["options"]; + } +} + +/** + * Set coding options for an encoder / decoder instance (will / should be deleted, when finish()/delete()) + * + * @param {Number} p_coder + * the encoder/decoder pointer (ID) + * @param {CodingOptions} options + * the coding options + * @private + * @memberOf Flac + */ +function _setOptions(p_coder, options){ + if(!coders[p_coder]){ + coders[p_coder] = {}; + } + coders[p_coder]["options"] = options; +} + +//(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame, void *client_data) +// -> FLAC__StreamEncoderWriteStatus +var enc_write_fn_ptr = addFunction(function(p_encoder, buffer, bytes, samples, current_frame, p_client_data){ + var retdata = new Uint8Array(bytes); + retdata.set(HEAPU8.subarray(buffer, buffer + bytes)); + var write_callback_fn = getCallback(p_encoder, 'write'); + try{ + write_callback_fn(retdata, bytes, samples, current_frame, p_client_data); + } catch(err) { + console.error(err); + return FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; + } + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; +}, 'iiiiiii'); + +//(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data) +// -> FLAC__StreamDecoderReadStatus +var dec_read_fn_ptr = addFunction(function(p_decoder, buffer, bytes, p_client_data){ + //FLAC__StreamDecoderReadCallback, see https://xiph.org/flac/api/group__flac__stream__decoder.html#ga7a5f593b9bc2d163884348b48c4285fd + + var len = Module.getValue(bytes, 'i32'); + + if(len === 0){ + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } + + var read_callback_fn = getCallback(p_decoder, 'read'); + + //callback must return object with: {buffer: TypedArray, readDataLength: number, error: boolean} + var readResult = read_callback_fn(len, p_client_data); + //in case of END_OF_STREAM or an error, readResult.readDataLength must be returned with 0 + + var readLen = readResult.readDataLength; + Module.setValue(bytes, readLen, 'i32'); + + if(readResult.error){ + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } + + if(readLen === 0){ + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + } + + var readBuf = readResult.buffer; + + var dataHeap = new Uint8Array(Module.HEAPU8.buffer, buffer, readLen); + dataHeap.set(new Uint8Array(readBuf)); + + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; +}, 'iiiii'); + +//(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) +// -> FLAC__StreamDecoderWriteStatus +var dec_write_fn_ptr = addFunction(function(p_decoder, p_frame, p_buffer, p_client_data){ + + // var dec = Module.getValue(p_decoder,'i32'); + // var clientData = Module.getValue(p_client_data,'i32'); + + var dec_opts = _getOptions(p_decoder); + var frameInfo = _readFrameHdr(p_frame, dec_opts); + +// console.log(frameInfo);//DEBUG + + var channels = frameInfo.channels; + var block_size = frameInfo.blocksize * (frameInfo.bitsPerSample / 8); + + //whether or not to apply data fixing heuristics (e.g. not needed for 24-bit samples) + var isFix = frameInfo.bitsPerSample !== 24; + + //take padding bits into account for calculating buffer size + // -> seems to be done for uneven byte sizes, i.e. 1 (8 bits) and 3 (24 bits) + var padding = (frameInfo.bitsPerSample / 8)%2; + if(padding > 0){ + block_size += frameInfo.blocksize * padding; + } + + var data = [];//<- array for the data of each channel + var bufferOffset, _buffer; + + for(var i=0; i < channels; ++i){ + + bufferOffset = Module.getValue(p_buffer + (i*4),'i32'); + + _buffer = new Uint8Array(block_size); + //FIXME HACK for "strange" data (see helper function __fix_write_buffer) + __fix_write_buffer(bufferOffset, _buffer, isFix); + + data.push(_buffer.subarray(0, block_size)); + } + + var write_callback_fn = getCallback(p_decoder, 'write'); + var res = write_callback_fn(data, frameInfo);//, clientData); + + // FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE The write was OK and decoding can continue. + // FLAC__STREAM_DECODER_WRITE_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. + + return res !== false? FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE : FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; +}, 'iiiii'); + +/** + * Decoding error codes. + * + *
+ * If the error code is not known, value FLAC__STREAM_DECODER_ERROR__UNKNOWN__ is used. + * + * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC"} 0 An error in the stream caused the decoder to lose synchronization. + * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER"} 1 The decoder encountered a corrupted frame header. + * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"} 2 The frame's data did not match the CRC in the footer. + * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"} 3 The decoder encountered reserved fields in use in the stream. + * + * + * @interface FLAC__StreamDecoderErrorStatus + * @memberOf Flac + */ +var DecoderErrorCode = { + 0: 'FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC', + 1: 'FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER', + 2: 'FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH', + 3: 'FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM' +} + +//(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) +// -> void +var dec_error_fn_ptr = addFunction(function(p_decoder, err, p_client_data){ + + //err: + var msg = DecoderErrorCode[err] || 'FLAC__STREAM_DECODER_ERROR__UNKNOWN__';//<- this should never happen; + + var error_callback_fn = getCallback(p_decoder, 'error'); + error_callback_fn(err, msg, p_client_data); +}, 'viii'); + +//(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) -> void +//(const FLAC__StreamEncoder *encoder, const FLAC__StreamMetadata *metadata, void *client_data) -> void +var metadata_fn_ptr = addFunction(function(p_coder, p_metadata, p_client_data){ + /* + typedef struct { + FLAC__MetadataType type; + FLAC__bool is_last; + unsigned length; + union { + FLAC__StreamMetadata_StreamInfo stream_info; + FLAC__StreamMetadata_Padding padding; + FLAC__StreamMetadata_Application application; + FLAC__StreamMetadata_SeekTable seek_table; + FLAC__StreamMetadata_VorbisComment vorbis_comment; + FLAC__StreamMetadata_CueSheet cue_sheet; + FLAC__StreamMetadata_Picture picture; + FLAC__StreamMetadata_Unknown unknown; + } data; + } FLAC__StreamMetadata; + */ + + /* + FLAC__METADATA_TYPE_STREAMINFO STREAMINFO block + FLAC__METADATA_TYPE_PADDING PADDING block + FLAC__METADATA_TYPE_APPLICATION APPLICATION block + FLAC__METADATA_TYPE_SEEKTABLE SEEKTABLE block + FLAC__METADATA_TYPE_VORBIS_COMMENT VORBISCOMMENT block (a.k.a. FLAC tags) + FLAC__METADATA_TYPE_CUESHEET CUESHEET block + FLAC__METADATA_TYPE_PICTURE PICTURE block + FLAC__METADATA_TYPE_UNDEFINED marker to denote beginning of undefined type range; this number will increase as new metadata types are added + FLAC__MAX_METADATA_TYPE No type will ever be greater than this. There is not enough room in the protocol block. + */ + + var type = Module.getValue(p_metadata,'i32');//4 bytes + var is_last = Module.getValue(p_metadata+4,'i32');//4 bytes + var length = Module.getValue(p_metadata+8,'i64');//8 bytes + + var meta_data = { + type: type, + isLast: is_last, + length: length, + data: void(0) + }; + + var metadata_callback_fn = getCallback(p_coder, 'metadata'); + if(type === 0){// === FLAC__METADATA_TYPE_STREAMINFO + + meta_data.data = _readStreamInfo(p_metadata+16); + metadata_callback_fn(meta_data.data, meta_data); + + } else { + + var data; + switch(type){ + case 1: //FLAC__METADATA_TYPE_PADDING + data = _readPaddingMetadata(p_metadata+16); + break; + case 2: //FLAC__METADATA_TYPE_APPLICATION + data = readApplicationMetadata(p_metadata+16); + break; + case 3: //FLAC__METADATA_TYPE_SEEKTABLE + data = _readSeekTableMetadata(p_metadata+16); + break; + + case 4: //FLAC__METADATA_TYPE_VORBIS_COMMENT + data = _readVorbisComment(p_metadata+16); + break; + + case 5: //FLAC__METADATA_TYPE_CUESHEET + data = _readCueSheetMetadata(p_metadata+16); + break; + + case 6: //FLAC__METADATA_TYPE_PICTURE + data = _readPictureMetadata(p_metadata+16); + break; + default: { //NOTE this should not happen, and the raw data is very likely not correct! + var cod_opts = _getOptions(p_coder); + if(cod_opts && cod_opts.enableRawMetadata){ + var buffer = Uint8Array.from(HEAPU8.subarray(p_metadata+16, p_metadata+16+length)); + meta_data.raw = buffer; + } + } + + } + + meta_data.data = data; + metadata_callback_fn(void(0), meta_data); + } + +}, 'viii'); + + +////////////// helper fields and functions for event handling +// see exported on()/off() functions +var listeners = {}; +var persistedEvents = []; +var add_event_listener = function (eventName, listener){ + var list = listeners[eventName]; + if(!list){ + list = [listener]; + listeners[eventName] = list; + } else { + list.push(listener); + } + check_and_trigger_persisted_event(eventName, listener); +}; +var check_and_trigger_persisted_event = function(eventName, listener){ + var activated; + for(var i=persistedEvents.length-1; i >= 0; --i){ + activated = persistedEvents[i]; + if(activated && activated.event === eventName){ + listener.apply(null, activated.args); + break; + } + } +}; +var remove_event_listener = function (eventName, listener){ + var list = listeners[eventName]; + if(list){ + for(var i=list.length-1; i >= 0; --i){ + if(list[i] === listener){ + list.splice(i, 1); + } + } + } +}; +/** + * HELPER: fire an event + * @param {string} eventName + * the event name + * @param {any[]} [args] OPITIONAL + * the arguments when triggering the listeners + * @param {boolean} [isPersist] OPTIONAL (positinal argument!) + * if TRUE, handlers for this event that will be registered after this will get triggered immediately + * (i.e. event is "persistent": once triggered it stays "active") + * + */ +var do_fire_event = function (eventName, args, isPersist){ + if(_exported['on'+eventName]){ + _exported['on'+eventName].apply(null, args); + } + var list = listeners[eventName]; + if(list){ + for(var i=0, size=list.length; i < size; ++i){ + list[i].apply(null, args) + } + } + if(isPersist){ + persistedEvents.push({event: eventName, args: args}); + } +} + +///////////////////////////////////// export / public: ///////////////////////////////////////////// +/** + * The Flac module that provides functionality + * for encoding WAV/PCM audio to Flac and decoding Flac to PCM. + * + *

+ *

+ * NOTE most functions are named analogous to the original C library functions, + * so that its documentation may be used for further reading. + *

+ * + * @see https://xiph.org/flac/api/group__flac__stream__encoder.html + * @see https://xiph.org/flac/api/group__flac__stream__decoder.html + * + * @class Flac + * @namespace Flac + */ +var _exported = { + _module: Module,//internal: reference to Flac module + _clear_enc_cb: function(enc_ptr){//internal function: remove reference to encoder instance and its callbacks + delete coders[enc_ptr]; + }, + _clear_dec_cb: function(dec_ptr){//internal function: remove reference to decoder instance and its callbacks + delete coders[dec_ptr]; + }, + /** + * Additional options for encoding or decoding + * @interface CodingOptions + * @memberOf Flac + * @property {boolean} [analyseSubframes] for decoding: include subframes metadata in write-callback metadata, DEFAULT: false + * @property {boolean} [analyseResiduals] for decoding: include residual data in subframes metadata in write-callback metadata, NOTE {@link #analyseSubframes} muste also be enabled, DEFAULT: false + * @property {boolean} [enableRawMetadata] DEBUG option for decoding: enable receiving raw metadata for unknown metadata types in second argument in the metadata-callback, DEFAULT: false + * + * @see Flac#setOptions + * @see Flac~metadata_callback_fn + * @see Flac#FLAC__stream_decoder_set_metadata_respond_all + */ + /** + * FLAC raw metadata + * + * @interface MetadataBlock + * @memberOf Flac + * @property {Flac.FLAC__MetadataType} type the type of the metadata + * @property {boolean} isLast if it is the last block of metadata + * @property {number} length the length of the metadata block (bytes) + * @property {Flac.StreamMetadata | Flac.PaddingMetadata | Flac.ApplicationMetadata | Flac.SeekTableMetadata | Flac.CueSheetMetadata | Flac.PictureMetadata} [data] the metadata (omitted for unknown metadata types) + * @property {Uint8Array} [raw] raw metadata (for debugging: enable via {@link Flac#setOptions}) + */ + /** + * FLAC padding metadata block + * + * @interface PaddingMetadata + * @memberOf Flac + * @property {number} dummy Conceptually this is an empty struct since we don't store the padding bytes. Empty structs are not allowed by some C compilers, hence the dummy. + * + * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_PADDING + */ + /** + * FLAC application metadata block + * + * NOTE the application meta data type is not really supported, i.e. the + * (binary) data is only a pointer to the memory heap. + * + * @interface ApplicationMetadata + * @memberOf Flac + * @property {number} id the application ID + * @property {number} data (pointer) + * + * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_APPLICATION + * @see application block format specification + */ + /** + * FLAC seek table metadata block + * + *

+ * From the format specification: + * + * The seek points must be sorted by ascending sample number. + * + * Each seek point's sample number must be the first sample of the target frame. + * + * Each seek point's sample number must be unique within the table + * + * Existence of a SEEKTABLE block implies a correct setting of total_samples in the stream_info block. + * + * Behavior is undefined when more than one SEEKTABLE block is present in a stream. + * + * @interface SeekTableMetadata + * @memberOf Flac + * @property {number} num_points the number of seek points + * @property {Flac.SeekPoint[]} points the seek points + * + * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_SEEKTABLE + */ + /** + * FLAC seek point data + * + * @interface SeekPoint + * @memberOf Flac + * @property {number} sample_number The sample number of the target frame. NOTE -1 for a placeholder point. + * @property {number} stream_offset The offset, in bytes, of the target frame with respect to beginning of the first frame. + * @property {number} frame_samples The number of samples in the target frame. + * + * @see Flac.SeekTableMetadata + */ + /** + * FLAC vorbis comment metadata block + * + * @interface VorbisCommentMetadata + * @memberOf Flac + * @property {string} vendor_string the vendor string + * @property {number} num_comments the number of comments + * @property {string[]} comments the comments + * + * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_VORBIS_COMMENT + */ + /** + * FLAC cue sheet metadata block + * + * @interface CueSheetMetadata + * @memberOf Flac + * @property {string} media_catalog_number Media catalog number, in ASCII printable characters 0x20-0x7e. In general, the media catalog number may be 0 to 128 bytes long. + * @property {number} lead_in The number of lead-in samples. + * @property {boolean} is_cd true if CUESHEET corresponds to a Compact Disc, else false. + * @property {number} num_tracks The number of tracks. + * @property {Flac.CueSheetTrack[]} tracks the tracks + * + * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_CUESHEET + */ + /** + * FLAC cue sheet track data + * + * @interface CueSheetTrack + * @memberOf Flac + * @property {number} offset Track offset in samples, relative to the beginning of the FLAC audio stream. + * @property {number} number The track number. + * @property {string} isrc Track ISRC. This is a 12-digit alphanumeric code. + * @property {"AUDIO" | "NON_AUDIO"} type The track type: audio or non-audio. + * @property {boolean} pre_emphasis The pre-emphasis flag + * @property {number} num_indices The number of track index points. + * @property {Flac.CueSheetTracIndex} indices The track index points. + * + * @see Flac.CueSheetMetadata + */ + /** + * FLAC track index data for cue sheet metadata + * + * @interface CueSheetTracIndex + * @memberOf Flac + * @property {number} offset Offset in samples, relative to the track offset, of the index point. + * @property {number} number The index point number. + * + * @see Flac.CueSheetTrack + */ + /** + * FLAC picture metadata block + * + * @interface PictureMetadata + * @memberOf Flac + * @property {Flac.FLAC__StreamMetadata_Picture_Type} type The kind of picture stored. + * @property {string} mime_type Picture data's MIME type, in ASCII printable characters 0x20-0x7e, NUL terminated. For best compatibility with players, use picture data of MIME type image/jpeg or image/png. A MIME type of '–>' is also allowed, in which case the picture data should be a complete URL. + * @property {string} description Picture's description. + * @property {number} width Picture's width in pixels. + * @property {number} height Picture's height in pixels. + * @property {number} depth Picture's color depth in bits-per-pixel. + * @property {number} colors For indexed palettes (like GIF), picture's number of colors (the number of palette entries), or 0 for non-indexed (i.e. 2^depth). + * @property {number} data_length Length of binary picture data in bytes. + * @property {Uint8Array} data Binary picture data. + */ + /** + * An enumeration of the PICTURE types (see FLAC__StreamMetadataPicture and id3 v2.4 APIC tag). + * + * @interface FLAC__StreamMetadata_Picture_Type + * @memberOf Flac + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_OTHER"} 0 Other + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON_STANDARD"} 1 32x32 pixels 'file icon' (PNG only) + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON"} 2 Other file icon + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FRONT_COVER"} 3 Cover (front) + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BACK_COVER"} 4 Cover (back) + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LEAFLET_PAGE"} 5 Leaflet page + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_MEDIA"} 6 Media (e.g. label side of CD) + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LEAD_ARTIST"} 7 Lead artist/lead performer/soloist + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_ARTIST"} 8 Artist/performer + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_CONDUCTOR"} 9 Conductor + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BAND"} 10 Band/Orchestra + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_COMPOSER"} 11 Composer + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LYRICIST"} 12 Lyricist/text writer + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_RECORDING_LOCATION"} 13 Recording Location + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_RECORDING"} 14 During recording + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_PERFORMANCE"} 15 During performance + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_VIDEO_SCREEN_CAPTURE"} 16 Movie/video screen capture + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FISH"} 17 A bright coloured fish + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_ILLUSTRATION"} 18 Illustration + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BAND_LOGOTYPE"} 19 Band/artist logotype + * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_PUBLISHER_LOGOTYPE"} 20 Publisher/Studio logotype + * + * @see Flac.PictureMetadata + */ + + /** + * An enumeration of the available metadata block types. + * + * @interface FLAC__MetadataType + * @memberOf Flac + * + * @property {"FLAC__METADATA_TYPE_STREAMINFO"}  0 STREAMINFO block + * @property {"FLAC__METADATA_TYPE_PADDING"}  1 PADDING block + * @property {"FLAC__METADATA_TYPE_APPLICATION"}  2 APPLICATION block + * @property {"FLAC__METADATA_TYPE_SEEKTABLE"}  3 SEEKTABLE block + * @property {"FLAC__METADATA_TYPE_VORBIS_COMMENT"}  4 VORBISCOMMENT block (a.k.a. FLAC tags) + * @property {"FLAC__METADATA_TYPE_CUESHEET"}  5 CUESHEET block + * @property {"FLAC__METADATA_TYPE_PICTURE"}  6 PICTURE block + * @property {"FLAC__METADATA_TYPE_UNDEFINED"}  7 marker to denote beginning of undefined type range; this number will increase as new metadata types are added + * @property {"FLAC__MAX_METADATA_TYPE"}  126 No type will ever be greater than this. There is not enough room in the protocol block. + * + * @see Flac.MetadataBlock + * @see FLAC format documentation + */ + /** + * @function + * @public + * @memberOf Flac# + * @copydoc Flac._setOptions + */ + setOptions: _setOptions, + /** + * @function + * @public + * @memberOf Flac# + * @copydoc Flac._getOptions + */ + getOptions: _getOptions, + /** + * Returns if Flac has been initialized / is ready to be used. + * + * @returns {boolean} true, if Flac is ready to be used + * + * @memberOf Flac# + * @function + * @see #onready + * @see #on + */ + isReady: function() { return _flac_ready; }, + /** + * Hook for handler function that gets called, when asynchronous initialization has finished. + * + * NOTE that if the execution environment does not support Object#defineProperty, then + * this function is not called, after {@link #isReady} is true. + * In this case, {@link #isReady} should be checked, before setting onready + * and if it is true, handler should be executed immediately instead of setting onready. + * + * @memberOf Flac# + * @function + * @param {Flac.event:ReadyEvent} event the ready-event object + * @see #isReady + * @see #on + * @default undefined + * @example + * // [1] if Object.defineProperty() IS supported: + * Flac.onready = function(event){ + * //gets executed when library becomes ready, or immediately, if it already is ready... + * doSomethingWithFlac(); + * }; + * + * // [2] if Object.defineProperty() is NOT supported: + * // do check Flac.isReady(), and only set handler, if not ready yet + * // (otherwise immediately excute handler code) + * if(!Flac.isReady()){ + * Flac.onready = function(event){ + * //gets executed when library becomes ready... + * doSomethingWithFlac(); + * }; + * } else { + * // Flac is already ready: immediately start processing + * doSomethingWithFlac(); + * } + */ + onready: void(0), + /** + * Ready event: is fired when the library has been initialized and is ready to be used + * (e.g. asynchronous loading of binary / WASM modules has been completed). + * + * Before this event is fired, use of functions related to encoding and decoding may + * cause errors. + * + * @event ReadyEvent + * @memberOf Flac + * @type {object} + * @property {"ready"} type the type of the event "ready" + * @property {Flac} target the initalized FLAC library instance + * + * @see #isReady + * @see #on + */ + /** + * Created event: is fired when an encoder or decoder was created. + * + * @event CreatedEvent + * @memberOf Flac + * @type {object} + * @property {"created"} type the type of the event "created" + * @property {Flac.CoderChangedEventData} target the information for the created encoder or decoder + * + * @see #on + */ + /** + * Destroyed event: is fired when an encoder or decoder was destroyed. + * + * @event DestroyedEvent + * @memberOf Flac + * @type {object} + * @property {"destroyed"} type the type of the event "destroyed" + * @property {Flac.CoderChangedEventData} target the information for the destroyed encoder or decoder + * + * @see #on + */ + /** + * Life cycle event data for signaling life cycle changes of encoder or decoder instances + * @interface CoderChangedEventData + * @memberOf Flac + * @property {number} id the ID for the encoder or decoder instance + * @property {"encoder" | "decoder"} type signifies whether the event is for an encoder or decoder instance + * @property {any} [data] specific data for the life cycle change + * + * @see Flac.event:CreatedEvent + * @see Flac.event:DestroyedEvent + */ + /** + * Add an event listener for module-events. + * Supported events: + *

    + *
  • "ready" → {@link Flac.event:ReadyEvent}: emitted when module is ready for usage (i.e. {@link #isReady} is true)
    + * NOTE listener will get immediately triggered if module is already "ready" + *
  • + *
  • "created" → {@link Flac.event:CreatedEvent}: emitted when an encoder or decoder instance was created
    + *
  • + *
  • "destroyed" → {@link Flac.event:DestroyedEvent}: emitted when an encoder or decoder instance was destroyed
    + *
  • + *
+ * + * @param {string} eventName + * @param {Function} listener + * + * @memberOf Flac# + * @function + * @see #off + * @see #onready + * @see Flac.event:ReadyEvent + * @see Flac.event:CreatedEvent + * @see Flac.event:DestroyedEvent + * @example + * Flac.on('ready', function(event){ + * //gets executed when library is ready, or becomes ready... + * }); + */ + on: add_event_listener, + /** + * Remove an event listener for module-events. + * @param {string} eventName + * @param {Function} listener + * + * @memberOf Flac# + * @function + * @see #on + */ + off: remove_event_listener, + + /** + * Set the "verify" flag. If true, the encoder will verify it's own encoded output by feeding it through an internal decoder and comparing the original signal against the decoded signal. If a mismatch occurs, the process call will return false. Note that this will slow the encoding process by the extra time required for decoding and comparison. + * + *

+ * NOTE: only use on un-initilized encoder instances! + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {boolean} is_verify enable/disable checksum verification during encoding + * + * @returns {boolean} false if the encoder is already initialized, else true + * + * @see #create_libflac_encoder + * @see #FLAC__stream_encoder_get_verify + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_set_verify: function(encoder, is_verify){ + is_verify = is_verify? 1 : 0; + Module.ccall('FLAC__stream_encoder_set_verify', 'number', ['number', 'number'], [ encoder, is_verify ]); + }, + /** + * Set the compression level + * + * The compression level is roughly proportional to the amount of effort the encoder expends to compress the file. A higher level usually means more computation but higher compression. The default level is suitable for most applications. + * + * Currently the levels range from 0 (fastest, least compression) to 8 (slowest, most compression). A value larger than 8 will be treated as 8. + * + * + *

+ * NOTE: only use on un-initilized encoder instances! + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {Flac.CompressionLevel} compression_level the desired Flac compression level: [0, 8] + * + * @returns {boolean} false if the encoder is already initialized, else true + * + * @see #create_libflac_encoder + * @see Flac.CompressionLevel + * @see FLAC API for FLAC__stream_encoder_set_compression_level() + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_set_compression_level: Module.cwrap('FLAC__stream_encoder_set_compression_level', 'number', [ 'number', 'number' ]), + /** + * Set the blocksize to use while encoding. + * The number of samples to use per frame. Use 0 to let the encoder estimate a blocksize; this is usually best. + * + *

+ * NOTE: only use on un-initilized encoder instances! + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {number} block_size the number of samples to use per frame + * + * @returns {boolean} false if the encoder is already initialized, else true + * + * @see #create_libflac_encoder + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_set_blocksize: Module.cwrap('FLAC__stream_encoder_set_blocksize', 'number', [ 'number', 'number']), + + + /** + * Get the state of the verify stream decoder. Useful when the stream encoder state is FLAC__STREAM_ENCODER_VERIFY_DECODER_ERROR. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @returns {Flac.FLAC__StreamDecoderState} the verify stream decoder state + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_get_verify_decoder_state: Module.cwrap('FLAC__stream_encoder_get_verify_decoder_state', 'number', ['number']), + + /** + * Get the "verify" flag for the encoder. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @returns {boolean} the verify flag for the encoder + * + * + * @memberOf Flac# + * @function + * + * @see #FLAC__stream_encoder_set_verify + */ + FLAC__stream_encoder_get_verify: Module.cwrap('FLAC__stream_encoder_get_verify', 'number', ['number']), +/* + +TODO export other encoder API functions?: + +FLAC__bool FLAC__stream_encoder_set_channels (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_bits_per_sample (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_sample_rate (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_do_mid_side_stereo (FLAC__StreamEncoder *encoder, FLAC__bool value) + +FLAC__bool FLAC__stream_encoder_set_loose_mid_side_stereo (FLAC__StreamEncoder *encoder, FLAC__bool value) + +FLAC__bool FLAC__stream_encoder_set_apodization (FLAC__StreamEncoder *encoder, const char *specification) + +FLAC__bool FLAC__stream_encoder_set_max_lpc_order (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_qlp_coeff_precision (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_do_qlp_coeff_prec_search (FLAC__StreamEncoder *encoder, FLAC__bool value) + +FLAC__bool FLAC__stream_encoder_set_do_escape_coding (FLAC__StreamEncoder *encoder, FLAC__bool value) + +FLAC__bool FLAC__stream_encoder_set_do_exhaustive_model_search (FLAC__StreamEncoder *encoder, FLAC__bool value) + +FLAC__bool FLAC__stream_encoder_set_min_residual_partition_order (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_max_residual_partition_order (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_set_rice_parameter_search_dist (FLAC__StreamEncoder *encoder, unsigned value) + +FLAC__bool FLAC__stream_encoder_get_streamable_subset (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_channels (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_bits_per_sample (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_sample_rate (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_blocksize (const FLAC__StreamEncoder *encoder) + +FLAC__bool FLAC__stream_encoder_get_do_mid_side_stereo (const FLAC__StreamEncoder *encoder) + +FLAC__bool FLAC__stream_encoder_get_loose_mid_side_stereo (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_max_lpc_order (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_qlp_coeff_precision (const FLAC__StreamEncoder *encoder) + +FLAC__bool FLAC__stream_encoder_get_do_qlp_coeff_prec_search (const FLAC__StreamEncoder *encoder) + +FLAC__bool FLAC__stream_encoder_get_do_escape_coding (const FLAC__StreamEncoder *encoder) + +FLAC__bool FLAC__stream_encoder_get_do_exhaustive_model_search (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_min_residual_partition_order (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_max_residual_partition_order (const FLAC__StreamEncoder *encoder) + +unsigned FLAC__stream_encoder_get_rice_parameter_search_dist (const FLAC__StreamEncoder *encoder) + +FLAC__uint64 FLAC__stream_encoder_get_total_samples_estimate (const FLAC__StreamEncoder *encoder) + + + +TODO export other decoder API functions?: + + +const char * FLAC__stream_decoder_get_resolved_state_string (const FLAC__StreamDecoder *decoder) + +FLAC__uint64 FLAC__stream_decoder_get_total_samples (const FLAC__StreamDecoder *decoder) + +unsigned FLAC__stream_decoder_get_channels (const FLAC__StreamDecoder *decoder) + +unsigned FLAC__stream_decoder_get_bits_per_sample (const FLAC__StreamDecoder *decoder) + +unsigned FLAC__stream_decoder_get_sample_rate (const FLAC__StreamDecoder *decoder) + +unsigned FLAC__stream_decoder_get_blocksize (const FLAC__StreamDecoder *decoder) + + +FLAC__bool FLAC__stream_decoder_flush (FLAC__StreamDecoder *decoder) + +FLAC__bool FLAC__stream_decoder_skip_single_frame (FLAC__StreamDecoder *decoder) + + */ + + /** + * Set the compression level + * + * The compression level is roughly proportional to the amount of effort the encoder expends to compress the file. A higher level usually means more computation but higher compression. The default level is suitable for most applications. + * + * Currently the levels range from 0 (fastest, least compression) to 8 (slowest, most compression). A value larger than 8 will be treated as 8. + * + * This function automatically calls the following other set functions with appropriate values, so the client does not need to unless it specifically wants to override them: + *

+	 *     FLAC__stream_encoder_set_do_mid_side_stereo()
+	 *     FLAC__stream_encoder_set_loose_mid_side_stereo()
+	 *     FLAC__stream_encoder_set_apodization()
+	 *     FLAC__stream_encoder_set_max_lpc_order()
+	 *     FLAC__stream_encoder_set_qlp_coeff_precision()
+	 *     FLAC__stream_encoder_set_do_qlp_coeff_prec_search()
+	 *     FLAC__stream_encoder_set_do_escape_coding()
+	 *     FLAC__stream_encoder_set_do_exhaustive_model_search()
+	 *     FLAC__stream_encoder_set_min_residual_partition_order()
+	 *     FLAC__stream_encoder_set_max_residual_partition_order()
+	 *     FLAC__stream_encoder_set_rice_parameter_search_dist()
+	 * 
+ * The actual values set for each level are: + * | level | do mid-side stereo | loose mid-side stereo | apodization | max lpc order | qlp coeff precision | qlp coeff prec search | escape coding | exhaustive model search | min residual partition order | max residual partition order | rice parameter search dist | + * |--------|---------------------|------------------------|------------------------------------------------|----------------|----------------------|------------------------|----------------|--------------------------|-------------------------------|-------------------------------|------------------------------| + * | 0 | false | false | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | + * | 1 | true | true | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | + * | 2 | true | false | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | + * | 3 | false | false | tukey(0.5) | 6 | 0 | false | false | false | 0 | 4 | 0 | + * | 4 | true | true | tukey(0.5) | 8 | 0 | false | false | false | 0 | 4 | 0 | + * | 5 | true | false | tukey(0.5) | 8 | 0 | false | false | false | 0 | 5 | 0 | + * | 6 | true | false | tukey(0.5);partial_tukey(2) | 8 | 0 | false | false | false | 0 | 6 | 0 | + * | 7 | true | false | tukey(0.5);partial_tukey(2) | 12 | 0 | false | false | false | 0 | 6 | 0 | + * | 8 | true | false | tukey(0.5);partial_tukey(2);punchout_tukey(3) | 12 | 0 | false | false | false | 0 | 6 | 0 | + * + * @interface CompressionLevel + * @memberOf Flac + * + * @property {"FLAC__COMPRESSION_LEVEL_0"} 0 compression level 0 + * @property {"FLAC__COMPRESSION_LEVEL_1"} 1 compression level 1 + * @property {"FLAC__COMPRESSION_LEVEL_2"} 2 compression level 2 + * @property {"FLAC__COMPRESSION_LEVEL_3"} 3 compression level 3 + * @property {"FLAC__COMPRESSION_LEVEL_4"} 4 compression level 4 + * @property {"FLAC__COMPRESSION_LEVEL_5"} 5 compression level 5 + * @property {"FLAC__COMPRESSION_LEVEL_6"} 6 compression level 6 + * @property {"FLAC__COMPRESSION_LEVEL_7"} 7 compression level 7 + * @property {"FLAC__COMPRESSION_LEVEL_8"} 8 compression level 8 + */ + /** + * Create an encoder. + * + * @param {number} sample_rate + * the sample rate of the input PCM data + * @param {number} channels + * the number of channels of the input PCM data + * @param {number} bps + * bits per sample of the input PCM data + * @param {Flac.CompressionLevel} compression_level + * the desired Flac compression level: [0, 8] + * @param {number} [total_samples] OPTIONAL + * the number of total samples of the input PCM data:
+ * Sets an estimate of the total samples that will be encoded. + * This is merely an estimate and may be set to 0 if unknown. + * This value will be written to the STREAMINFO block before encoding, + * and can remove the need for the caller to rewrite the value later if + * the value is known before encoding.
+ * If specified, the it will be written into metadata of the FLAC header.
+ * DEFAULT: 0 (i.e. unknown number of samples) + * @param {boolean} [is_verify] OPTIONAL + * enable/disable checksum verification during encoding
+ * DEFAULT: true
+ * NOTE: this argument is positional (i.e. total_samples must also be given) + * @param {number} [block_size] OPTIONAL + * the number of samples to use per frame.
+ * DEFAULT: 0 (i.e. encoder sets block size automatically) + * NOTE: this argument is positional (i.e. total_samples and is_verify must also be given) + * + * + * @returns {number} the ID of the created encoder instance (or 0, if there was an error) + * + * @memberOf Flac# + * @function + */ + create_libflac_encoder: function(sample_rate, channels, bps, compression_level, total_samples, is_verify, block_size){ + is_verify = typeof is_verify === 'undefined'? 1 : is_verify + 0; + total_samples = typeof total_samples === 'number'? total_samples : 0; + block_size = typeof block_size === 'number'? block_size : 0; + var ok = true; + var encoder = Module.ccall('FLAC__stream_encoder_new', 'number', [ ], [ ]); + ok &= Module.ccall('FLAC__stream_encoder_set_verify', 'number', ['number', 'number'], [ encoder, is_verify ]); + ok &= Module.ccall('FLAC__stream_encoder_set_compression_level', 'number', ['number', 'number'], [ encoder, compression_level ]); + ok &= Module.ccall('FLAC__stream_encoder_set_channels', 'number', ['number', 'number'], [ encoder, channels ]); + ok &= Module.ccall('FLAC__stream_encoder_set_bits_per_sample', 'number', ['number', 'number'], [ encoder, bps ]); + ok &= Module.ccall('FLAC__stream_encoder_set_sample_rate', 'number', ['number', 'number'], [ encoder, sample_rate ]); + ok &= Module.ccall('FLAC__stream_encoder_set_blocksize', 'number', [ 'number', 'number'], [ encoder, block_size ]); + ok &= Module.ccall('FLAC__stream_encoder_set_total_samples_estimate', 'number', ['number', 'number'], [ encoder, total_samples ]); + if (ok){ + do_fire_event('created', [{type: 'created', target: {id: encoder, type: 'encoder'}}], false); + return encoder; + } + return 0; + }, + /** + * @deprecated use {@link #create_libflac_encoder} instead + * @memberOf Flac# + * @function + */ + init_libflac_encoder: function(){ + console.warn('Flac.init_libflac_encoder() is deprecated, use Flac.create_libflac_encoder() instead!'); + return this.create_libflac_encoder.apply(this, arguments); + }, + + /** + * Create a decoder. + * + * @param {boolean} [is_verify] + * enable/disable checksum verification during decoding
+ * DEFAULT: true + * + * @returns {number} the ID of the created decoder instance (or 0, if there was an error) + * + * @memberOf Flac# + * @function + */ + create_libflac_decoder: function(is_verify){ + is_verify = typeof is_verify === 'undefined'? 1 : is_verify + 0; + var ok = true; + var decoder = Module.ccall('FLAC__stream_decoder_new', 'number', [ ], [ ]); + ok &= Module.ccall('FLAC__stream_decoder_set_md5_checking', 'number', ['number', 'number'], [ decoder, is_verify ]); + if (ok){ + do_fire_event('created', [{type: 'created', target: {id: decoder, type: 'decoder'}}], false); + return decoder; + } + return 0; + }, + /** + * @deprecated use {@link #create_libflac_decoder} instead + * @memberOf Flac# + * @function + */ + init_libflac_decoder: function(){ + console.warn('Flac.init_libflac_decoder() is deprecated, use Flac.create_libflac_decoder() instead!'); + return this.create_libflac_decoder.apply(this, arguments); + }, + /** + * The callback for writing the encoded FLAC data. + * + * @callback Flac~encoder_write_callback_fn + * @param {Uint8Array} data the encoded FLAC data + * @param {number} numberOfBytes the number of bytes in data + * @param {number} samples the number of samples encoded in data + * @param {number} currentFrame the number of the (current) encoded frame in data + * @returns {void | false} returning false indicates that an + * unrecoverable error occurred and decoding should be aborted + */ + /** + * The callback for the metadata of the encoded/decoded Flac data. + * + * By default, only the STREAMINFO metadata is enabled. + * + * For other metadata types {@link Flac.FLAC__MetadataType} they need to be enabled, + * see e.g. {@link Flac#FLAC__stream_decoder_set_metadata_respond} + * + * @callback Flac~metadata_callback_fn + * @param {Flac.StreamMetadata | undefined} metadata the FLAC meta data, NOTE only STREAMINFO is returned in first argument, for other types use 2nd argument's metadataBlock.data + * @param {Flac.MetadataBlock} metadataBlock the detailed meta data block + * + * @see Flac#init_decoder_stream + * @see Flac#init_encoder_stream + * @see Flac.CodingOptions + * @see Flac#FLAC__stream_decoder_set_metadata_respond_all + */ + /** + * FLAC meta data + * @interface Metadata + * @memberOf Flac + * @property {number} sampleRate the sample rate (Hz) + * @property {number} channels the number of channels + * @property {number} bitsPerSample bits per sample + */ + /** + * FLAC stream meta data + * @interface StreamMetadata + * @memberOf Flac + * @augments Flac.Metadata + * @property {number} min_blocksize the minimal block size (bytes) + * @property {number} max_blocksize the maximal block size (bytes) + * @property {number} min_framesize the minimal frame size (bytes) + * @property {number} max_framesize the maximal frame size (bytes) + * @property {number} total_samples the total number of (encoded/decoded) samples + * @property {string} md5sum the MD5 checksum for the decoded data (if validation is active) + */ + /** + * Initialize the encoder. + * + * @param {number} encoder + * the ID of the encoder instance that has not been initialized (or has been reset) + * + * @param {Flac~encoder_write_callback_fn} write_callback_fn + * the callback for writing the encoded Flac data: + *
write_callback_fn(data: Uint8Array, numberOfBytes: Number, samples: Number, currentFrame: Number)
+ * + * @param {Flac~metadata_callback_fn} [metadata_callback_fn] OPTIONAL + * the callback for the metadata of the encoded Flac data: + *
metadata_callback_fn(metadata: StreamMetadata)
+ * + * @param {number|boolean} [ogg_serial_number] OPTIONAL + * if number or true is specified, the encoder will be initialized to + * write to an OGG container, see {@link Flac.init_encoder_ogg_stream}: + * true will set a default serial number (1), + * if specified as number, it will be used as the stream's serial number within the ogg container. + * + * @returns {Flac.FLAC__StreamEncoderInitStatus} the encoder status (0 for FLAC__STREAM_ENCODER_INIT_STATUS_OK) + * + * @memberOf Flac# + * @function + */ + init_encoder_stream: function(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ + + var is_ogg = (ogg_serial_number === true); + client_data = client_data|0; + + if(typeof write_callback_fn !== 'function'){ + return FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS; + } + setCallback(encoder, 'write', write_callback_fn); + + var __metadata_callback_fn_ptr = 0; + if(typeof metadata_callback_fn === 'function'){ + setCallback(encoder, 'metadata', metadata_callback_fn); + __metadata_callback_fn_ptr = metadata_fn_ptr; + } + + //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): + // Module.ccall('FLAC__stream_encoder_init_stream' + var func_name = 'FLAC__stream_encoder_init_stream'; + var args_types = ['number', 'number', 'number', 'number', 'number', 'number']; + var args = [ + encoder, + enc_write_fn_ptr, + 0,// FLAC__StreamEncoderSeekCallback + 0,// FLAC__StreamEncoderTellCallback + __metadata_callback_fn_ptr, + client_data + ]; + + if(typeof ogg_serial_number === 'number'){ + + is_ogg = true; + + } else if(is_ogg){//else: set default serial number for stream in OGG container + + //NOTE from FLAC docs: "It is recommended to set a serial number explicitly as the default of '0' may collide with other streams." + ogg_serial_number = 1; + } + + if(is_ogg){ + //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): + // Module.ccall('FLAC__stream_encoder_init_ogg_stream' + func_name = 'FLAC__stream_encoder_init_ogg_stream'; + + //2nd arg: FLAC__StreamEncoderReadCallback ptr -> duplicate first entry & insert at [1] + args.unshift(args[0]); + args[1] = 0;// FLAC__StreamEncoderReadCallback + + args_types.unshift(args_types[0]); + args_types[1] = 'number'; + + + //NOTE ignore BOOL return value when setting serial number, since init-call's returned + // status will also indicate, if encoder already has been initialized + Module.ccall( + 'FLAC__stream_encoder_set_ogg_serial_number', 'number', + ['number', 'number'], + [ encoder, ogg_serial_number ] + ); + } + + var init_status = Module.ccall(func_name, 'number', args_types, args); + + return init_status; + }, + /** + * Initialize the encoder for writing to an OGG container. + * + * @param {number} [ogg_serial_number] OPTIONAL + * the serial number for the stream in the OGG container + * DEFAULT: 1 + * + * @memberOf Flac# + * @function + * @copydoc #init_encoder_stream + */ + init_encoder_ogg_stream: function(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ + + if(typeof ogg_serial_number !== 'number'){ + ogg_serial_number = true; + } + return this.init_encoder_stream(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data); + }, + /** + * Result / return value for {@link Flac~decoder_read_callback_fn} callback function + * + * @interface ReadResult + * @memberOf Flac + * @property {TypedArray} buffer a TypedArray (e.g. Uint8Array) with the read data + * @property {number} readDataLength the number of read data bytes. A number of 0 (zero) indicates that the end-of-stream is reached. + * @property {boolean} [error] OPTIONAL value of true indicates that an error occured (decoding will be aborted) + */ + /** + * Result / return value for {@link Flac~decoder_read_callback_fn} callback function for signifying that there is no more data to read + * + * @interface CompletedReadResult + * @memberOf Flac + * @augments Flac.ReadResult + * @property {TypedArray | undefined} buffer a TypedArray (e.g. Uint8Array) with the read data (will be ignored in case readDataLength is 0) + * @property {0} readDataLength the number of read data bytes: The number of 0 (zero) indicates that the end-of-stream is reached. + */ + /** + * The callback for reading the FLAC data that will be decoded. + * + * @callback Flac~decoder_read_callback_fn + * @param {number} numberOfBytes the maximal number of bytes that the read callback can return + * @returns {Flac.ReadResult | Flac.CompletedReadResult} the result of the reading action/request + */ + /** + * The callback for writing the decoded FLAC data. + * + * @callback Flac~decoder_write_callback_fn + * @param {Uint8Array[]} data array of the channels with the decoded PCM data as Uint8Arrays + * @param {Flac.BlockMetadata} frameInfo the metadata information for the decoded data + */ + /** + * The callback for reporting decoding errors. + * + * @callback Flac~decoder_error_callback_fn + * @param {number} errorCode the error code + * @param {Flac.FLAC__StreamDecoderErrorStatus} errorDescription the string representation / description of the error + */ + /** + * FLAC block meta data + * @interface BlockMetadata + * @augments Flac.Metadata + * @memberOf Flac + * + * @property {number} blocksize the block size (bytes) + * @property {number} number the number of the decoded samples or frames + * @property {string} numberType the type to which number refers to: either "frames" or "samples" + * @property {Flac.FLAC__ChannelAssignment} channelAssignment the channel assignment + * @property {string} crc the MD5 checksum for the decoded data, if validation is enabled + * @property {Flac.SubFrameMetadata[]} [subframes] the metadata of the subframes. The array length corresponds to the number of channels. NOTE will only be included if {@link Flac.CodingOptions CodingOptions.analyseSubframes} is enabled for the decoder. + * + * @see Flac.CodingOptions + * @see Flac#setOptions + */ + /** + * FLAC subframe metadata + * @interface SubFrameMetadata + * @memberOf Flac + * + * @property {Flac.FLAC__SubframeType} type the type of the subframe + * @property {number|Flac.FixedSubFrameData|Flac.LPCSubFrameData} data the type specific metadata for subframe + * @property {number} wastedBits the wasted bits-per-sample + */ + /** + * metadata for FIXED subframe type + * @interface FixedSubFrameData + * @memberOf Flac + * + * @property {number} order The polynomial order. + * @property {number[]} warmup Warmup samples to prime the predictor, length == order. + * @property {Flac.SubFramePartition} partition The residual coding method. + * @property {number[]} [residual] The residual signal, length == (blocksize minus order) samples. + * NOTE will only be included if {@link Flac.CodingOptions CodingOptions.analyseSubframes} is enabled for the decoder. + */ + /** + * metadata for LPC subframe type + * @interface LPCSubFrameData + * @augments Flac.FixedSubFrameData + * @memberOf Flac + * + * @property {number} order The FIR order. + * @property {number[]} qlp_coeff FIR filter coefficients. + * @property {number} qlp_coeff_precision Quantized FIR filter coefficient precision in bits. + * @property {number} quantization_level The qlp coeff shift needed. + */ + /** + * metadata for FIXED or LPC subframe partitions + * @interface SubFramePartition + * @memberOf Flac + * + * @property {Flac.FLAC__EntropyCodingMethodType} type the entropy coding method + * @property {Flac.SubFramePartitionData} data metadata for a Rice partitioned residual + */ + /** + * metadata for FIXED or LPC subframe partition data + * @interface SubFramePartitionData + * @memberOf Flac + * + * @property {number} order The partition order, i.e. # of contexts = 2 ^ order. + * @property {Flac.SubFramePartitionContent} contents The context's Rice parameters and/or raw bits. + */ + /** + * metadata for FIXED or LPC subframe partition data content + * @interface SubFramePartitionContent + * @memberOf Flac + * + * @property {number[]} parameters The Rice parameters for each context. + * @property {number[]} rawBits Widths for escape-coded partitions. Will be non-zero for escaped partitions and zero for unescaped partitions. + * @property {number} capacityByOrder The capacity of the parameters and raw_bits arrays specified as an order, i.e. the number of array elements allocated is 2 ^ capacity_by_order. + */ + /** + * The types for FLAC subframes + * + * @interface FLAC__SubframeType + * @memberOf Flac + * + * @property {"FLAC__SUBFRAME_TYPE_CONSTANT"}  0 constant signal + * @property {"FLAC__SUBFRAME_TYPE_VERBATIM"}  1 uncompressed signal + * @property {"FLAC__SUBFRAME_TYPE_FIXED"}  2 fixed polynomial prediction + * @property {"FLAC__SUBFRAME_TYPE_LPC"}  3 linear prediction + */ + /** + * The channel assignment for the (decoded) frame. + * + * @interface FLAC__ChannelAssignment + * @memberOf Flac + * + * @property {"FLAC__CHANNEL_ASSIGNMENT_INDEPENDENT"} 0 independent channels + * @property {"FLAC__CHANNEL_ASSIGNMENT_LEFT_SIDE"} 1 left+side stereo + * @property {"FLAC__CHANNEL_ASSIGNMENT_RIGHT_SIDE"} 2 right+side stereo + * @property {"FLAC__CHANNEL_ASSIGNMENT_MID_SIDE"} 3 mid+side stereo + */ + /** + * entropy coding methods + * + * @interface FLAC__EntropyCodingMethodType + * @memberOf Flac + * + * @property {"FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE"}  0 Residual is coded by partitioning into contexts, each with it's own 4-bit Rice parameter. + * @property {"FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2"}  1 Residual is coded by partitioning into contexts, each with it's own 5-bit Rice parameter. + */ + /** + * Initialize the decoder. + * + * @param {number} decoder + * the ID of the decoder instance that has not been initialized (or has been reset) + * + * @param {Flac~decoder_read_callback_fn} read_callback_fn + * the callback for reading the Flac data that should get decoded: + *
read_callback_fn(numberOfBytes: Number) : {buffer: ArrayBuffer, readDataLength: number, error: boolean}
+ * + * @param {Flac~decoder_write_callback_fn} write_callback_fn + * the callback for writing the decoded data: + *
write_callback_fn(data: Uint8Array[], frameInfo: Metadata)
+ * + * @param {Flac~decoder_error_callback_fn} error_callback_fn + * the error callback: + *
error_callback_fn(errorCode: Number, errorDescription: String)
+ * + * @param {Flac~metadata_callback_fn} [metadata_callback_fn] OPTIONAL + * callback for receiving the metadata of FLAC data that will be decoded: + *
metadata_callback_fn(metadata: StreamMetadata)
+ * + * @param {number|boolean} [ogg_serial_number] OPTIONAL + * if number or true is specified, the decoder will be initilized to + * read from an OGG container, see {@link Flac.init_decoder_ogg_stream}:
+ * true will use the default serial number, if specified as number the + * corresponding stream with the serial number from the ogg container will be used. + * + * @returns {Flac.FLAC__StreamDecoderInitStatus} the decoder status(0 for FLAC__STREAM_DECODER_INIT_STATUS_OK) + * + * @memberOf Flac# + * @function + */ + init_decoder_stream: function(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ + + client_data = client_data|0; + + if(typeof read_callback_fn !== 'function'){ + return FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS; + } + setCallback(decoder, 'read', read_callback_fn); + + if(typeof write_callback_fn !== 'function'){ + return FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS; + } + setCallback(decoder, 'write', write_callback_fn); + + var __error_callback_fn_ptr = 0; + if(typeof error_callback_fn === 'function'){ + setCallback(decoder, 'error', error_callback_fn); + __error_callback_fn_ptr = dec_error_fn_ptr; + } + + var __metadata_callback_fn_ptr = 0; + if(typeof metadata_callback_fn === 'function'){ + setCallback(decoder, 'metadata', metadata_callback_fn); + __metadata_callback_fn_ptr = metadata_fn_ptr; + } + + var is_ogg = (ogg_serial_number === true); + if(typeof ogg_serial_number === 'number'){ + + is_ogg = true; + + //NOTE ignore BOOL return value when setting serial number, since init-call's returned + // status will also indicate, if decoder already has been initialized + Module.ccall( + 'FLAC__stream_decoder_set_ogg_serial_number', 'number', + ['number', 'number'], + [ decoder, ogg_serial_number ] + ); + } + + //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): + // Module.ccall('FLAC__stream_decoder_init_stream' + // Module.ccall('FLAC__stream_decoder_init_ogg_stream' + var init_func_name = !is_ogg? 'FLAC__stream_decoder_init_stream' : 'FLAC__stream_decoder_init_ogg_stream'; + + var init_status = Module.ccall( + init_func_name, 'number', + [ 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'], + [ + decoder, + dec_read_fn_ptr, + 0,// FLAC__StreamDecoderSeekCallback + 0,// FLAC__StreamDecoderTellCallback + 0,// FLAC__StreamDecoderLengthCallback + 0,// FLAC__StreamDecoderEofCallback + dec_write_fn_ptr, + __metadata_callback_fn_ptr, + __error_callback_fn_ptr, + client_data + ] + ); + + return init_status; + }, + /** + * Initialize the decoder for writing to an OGG container. + * + * @param {number} [ogg_serial_number] OPTIONAL + * the serial number for the stream in the OGG container that should be decoded.
+ * The default behavior is to use the serial number of the first Ogg page. Setting a serial number here will explicitly specify which stream is to be decoded. + * + * @memberOf Flac# + * @function + * @copydoc #init_decoder_stream + */ + init_decoder_ogg_stream: function(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ + + if(typeof ogg_serial_number !== 'number'){ + ogg_serial_number = true; + } + return this.init_decoder_stream(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data); + }, + /** + * Encode / submit data for encoding. + * + * This version allows you to supply the input data where the channels are interleaved into a + * single array (i.e. channel0_sample0, channel1_sample0, ... , channelN_sample0, channel0_sample1, ...). + * + * The samples need not be block-aligned but they must be sample-aligned, i.e. the first value should be + * channel0_sample0 and the last value channelN_sampleM. + * + * Each sample should be a signed integer, right-justified to the resolution set by bits-per-sample. + * + * For example, if the resolution is 16 bits per sample, the samples should all be in the range [-32768,32767]. + * + * + * For applications where channel order is important, channels must follow the order as described in the frame header. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {TypedArray} buffer + * the audio data in a typed array with signed integers (and size according to the set bits-per-sample setting) + * + * @param {number} num_of_samples + * the number of samples in buffer + * + * @returns {boolean} true if successful, else false; in this case, check the encoder state with FLAC__stream_encoder_get_state() to see what went wrong. + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_process_interleaved: function(encoder, buffer, num_of_samples){ + // get the length of the data in bytes + var numBytes = buffer.length * buffer.BYTES_PER_ELEMENT; + // malloc enough space for the data + var ptr = Module._malloc(numBytes); + // get a bytes-wise view on the newly allocated buffer + var heapBytes= new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); + // copy data into heapBytes + heapBytes.set(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));// issue #11 (2): do use byteOffset and byteLength for copying the data in case the underlying buffer/ArrayBuffer of the TypedArray view is larger than the TypedArray + var status = Module.ccall('FLAC__stream_encoder_process_interleaved', 'number', + ['number', 'number', 'number'], + [encoder, heapBytes.byteOffset, num_of_samples] + ); + Module._free(ptr); + return status; + }, + + /** + * Encode / submit data for encoding. + * + * Submit data for encoding. This version allows you to supply the input data via an array of pointers, + * each pointer pointing to an array of samples samples representing one channel. + * The samples need not be block-aligned, but each channel should have the same number of samples. + * + * Each sample should be a signed integer, right-justified to the resolution set by FLAC__stream_encoder_set_bits_per_sample(). + * For example, if the resolution is 16 bits per sample, the samples should all be in the range [-32768,32767]. + * + * + * For applications where channel order is important, channels must follow the order as described in the frame header. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {TypedArray[]} channelBuffers + * an array for the audio data channels as typed arrays with signed integers (and size according to the set bits-per-sample setting) + * + * @param {number} num_of_samples + * the number of samples in one channel (i.e. one of the buffers) + * + * @returns {boolean} true if successful, else false; in this case, check the encoder state with FLAC__stream_encoder_get_state() to see what went wrong. + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_process: function(encoder, channelBuffers, num_of_samples){ + + var ptrInfo = this._create_pointer_array(channelBuffers); + var pointerPtr = ptrInfo.pointerPointer; + + var status = Module.ccall('FLAC__stream_encoder_process', 'number', + ['number', 'number', 'number'], + [encoder, pointerPtr, num_of_samples] + ); + + this._destroy_pointer_array(ptrInfo); + return status; + }, + /** + * Decodes a single frame. + * + * To check decoding progress, use {@link #FLAC__stream_decoder_get_state}. + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} FALSE if an error occurred + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_process_single: Module.cwrap('FLAC__stream_decoder_process_single', 'number', ['number']), + + /** + * Decodes data until end of stream. + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} FALSE if an error occurred + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_process_until_end_of_stream: Module.cwrap('FLAC__stream_decoder_process_until_end_of_stream', 'number', ['number']), + + /** + * Decodes data until end of metadata. + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} false if any fatal read, write, or memory allocation error occurred (meaning decoding must stop), else true. + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_process_until_end_of_metadata: Module.cwrap('FLAC__stream_decoder_process_until_end_of_metadata', 'number', ['number']), + + /** + * Decoder state code. + * + * @interface FLAC__StreamDecoderState + * @memberOf Flac + * + * @property {"FLAC__STREAM_DECODER_SEARCH_FOR_METADATA"} 0 The decoder is ready to search for metadata + * @property {"FLAC__STREAM_DECODER_READ_METADATA"} 1 The decoder is ready to or is in the process of reading metadata + * @property {"FLAC__STREAM_DECODER_SEARCH_FOR_FRAME_SYNC"} 2 The decoder is ready to or is in the process of searching for the frame sync code + * @property {"FLAC__STREAM_DECODER_READ_FRAME"} 3 The decoder is ready to or is in the process of reading a frame + * @property {"FLAC__STREAM_DECODER_END_OF_STREAM"} 4 The decoder has reached the end of the stream + * @property {"FLAC__STREAM_DECODER_OGG_ERROR"} 5 An error occurred in the underlying Ogg layer + * @property {"FLAC__STREAM_DECODER_SEEK_ERROR"} 6 An error occurred while seeking. The decoder must be flushed with FLAC__stream_decoder_flush() or reset with FLAC__stream_decoder_reset() before decoding can continue + * @property {"FLAC__STREAM_DECODER_ABORTED"} 7 The decoder was aborted by the read callback + * @property {"FLAC__STREAM_DECODER_MEMORY_ALLOCATION_ERROR"} 8 An error occurred allocating memory. The decoder is in an invalid state and can no longer be used + * @property {"FLAC__STREAM_DECODER_UNINITIALIZED"} 9 The decoder is in the uninitialized state; one of the FLAC__stream_decoder_init_*() functions must be called before samples can be processed. + * + */ + /** + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {Flac.FLAC__StreamDecoderState} the decoder state + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_get_state: Module.cwrap('FLAC__stream_decoder_get_state', 'number', ['number']), + + /** + * Encoder state code. + * + * @interface FLAC__StreamEncoderState + * @memberOf Flac + * + * @property {"FLAC__STREAM_ENCODER_OK"} 0 The encoder is in the normal OK state and samples can be processed. + * @property {"FLAC__STREAM_ENCODER_UNINITIALIZED"} 1 The encoder is in the uninitialized state; one of the FLAC__stream_encoder_init_*() functions must be called before samples can be processed. + * @property {"FLAC__STREAM_ENCODER_OGG_ERROR"} 2 An error occurred in the underlying Ogg layer. + * @property {"FLAC__STREAM_ENCODER_VERIFY_DECODER_ERROR"} 3 An error occurred in the underlying verify stream decoder; check FLAC__stream_encoder_get_verify_decoder_state(). + * @property {"FLAC__STREAM_ENCODER_VERIFY_MISMATCH_IN_AUDIO_DATA"} 4 The verify decoder detected a mismatch between the original audio signal and the decoded audio signal. + * @property {"FLAC__STREAM_ENCODER_CLIENT_ERROR"} 5 One of the callbacks returned a fatal error. + * @property {"FLAC__STREAM_ENCODER_IO_ERROR"} 6 An I/O error occurred while opening/reading/writing a file. Check errno. + * @property {"FLAC__STREAM_ENCODER_FRAMING_ERROR"} 7 An error occurred while writing the stream; usually, the write_callback returned an error. + * @property {"FLAC__STREAM_ENCODER_MEMORY_ALLOCATION_ERROR"} 8 Memory allocation failed. + * + */ + /** + * + * @param {number} encoder + * the ID of the encoder instance + * + * @returns {Flac.FLAC__StreamEncoderState} the encoder state + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_get_state: Module.cwrap('FLAC__stream_encoder_get_state', 'number', ['number']), + /** + * Direct the decoder to pass on all metadata blocks of type type. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @param {Flac.FLAC__MetadataType} type the metadata type to be enabled + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_respond_all + */ + FLAC__stream_decoder_set_metadata_respond: Module.cwrap('FLAC__stream_decoder_set_metadata_respond', 'number', ['number', 'number']), + /** + * Direct the decoder to pass on all APPLICATION metadata blocks of the given id. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @param {number} id the ID of application metadata + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_respond_all + */ + FLAC__stream_decoder_set_metadata_respond_application: Module.cwrap('FLAC__stream_decoder_set_metadata_respond_application', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, const FLAC__byte id[4]) + /** + * Direct the decoder to pass on all metadata blocks of any type. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all + * @see Flac#FLAC__stream_decoder_set_metadata_respond_application + * @see Flac#FLAC__stream_decoder_set_metadata_respond + */ + FLAC__stream_decoder_set_metadata_respond_all: Module.cwrap('FLAC__stream_decoder_set_metadata_respond_all', 'number', ['number']),// (FLAC__StreamDecoder *decoder) + /** + * Direct the decoder to filter out all metadata blocks of type type. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @param {Flac.FLAC__MetadataType} type the metadata type to be ignored + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all + */ + FLAC__stream_decoder_set_metadata_ignore: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, FLAC__MetadataType type) + /** + * Direct the decoder to filter out all APPLICATION metadata blocks of the given id. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @param {number} id the ID of application metadata + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all + */ + FLAC__stream_decoder_set_metadata_ignore_application: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore_application', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, const FLAC__byte id[4]) + /** + * Direct the decoder to filter out all metadata blocks of any type. + * + * By default, only the STREAMINFO block is returned via the metadata callback. + * + *

+ * NOTE: only use on un-initilized decoder instances! + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#FLAC__stream_decoder_set_metadata_respond_all + * @see Flac#FLAC__stream_decoder_set_metadata_ignore + * @see Flac#FLAC__stream_decoder_set_metadata_ignore_application + */ + FLAC__stream_decoder_set_metadata_ignore_all: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore_all', 'number', ['number']),// (FLAC__StreamDecoder *decoder) + /** + * Set the metadata blocks to be emitted to the stream before encoding. A value of NULL, 0 implies no metadata; otherwise, supply an array of pointers to metadata blocks. + * The array is non-const since the encoder may need to change the is_last flag inside them, and in some cases update seek point offsets. Otherwise, the encoder + * will not modify or free the blocks. It is up to the caller to free the metadata blocks after encoding finishes. + * + *

+ * The encoder stores only copies of the pointers in the metadata array; the metadata blocks themselves must survive at least until after FLAC__stream_encoder_finish() returns. + * Do not free the blocks until then. + * + * The STREAMINFO block is always written and no STREAMINFO block may occur in the supplied array. + * + * By default the encoder does not create a SEEKTABLE. If one is supplied in the metadata array, but the client has specified that it does not support seeking, + * then the SEEKTABLE will be written verbatim. However by itself this is not very useful as the client will not know the stream offsets for the seekpoints ahead of time. + * In order to get a proper seektable the client must support seeking. See next note. + * + * SEEKTABLE blocks are handled specially. Since you will not know the values for the seek point stream offsets, you should pass in a SEEKTABLE 'template', that is, + * a SEEKTABLE object with the required sample numbers (or placeholder points), with 0 for the frame_samples and stream_offset fields for each point. + * If the client has specified that it supports seeking by providing a seek callback to FLAC__stream_encoder_init_stream() or both seek AND read callback to + * FLAC__stream_encoder_init_ogg_stream() (or by using FLAC__stream_encoder_init*_file() or FLAC__stream_encoder_init*_FILE()), then while it is encoding the encoder will + * fill the stream offsets in for you and when encoding is finished, it will seek back and write the real values into the SEEKTABLE block in the stream. There are helper + * routines for manipulating seektable template blocks; see metadata.h: FLAC__metadata_object_seektable_template_*(). If the client does not support seeking, + * the SEEKTABLE will have inaccurate offsets which will slow down or remove the ability to seek in the FLAC stream. + * + * The encoder instance will modify the first SEEKTABLE block as it transforms the template to a valid seektable while encoding, but it is still up to the caller to free + * all metadata blocks after encoding. + * + * A VORBIS_COMMENT block may be supplied. The vendor string in it will be ignored. libFLAC will use it's own vendor string. libFLAC will not modify the passed-in + * VORBIS_COMMENT's vendor string, it will simply write it's own into the stream. If no VORBIS_COMMENT block is present in the metadata array, libFLAC will write an + * empty one, containing only the vendor string. + * + * The Ogg FLAC mapping requires that the VORBIS_COMMENT block be the second metadata block of the stream. The encoder already supplies the STREAMINFO block automatically. + * + * If metadata does not contain a VORBIS_COMMENT block, the encoder will supply that too. Otherwise, if metadata does contain a VORBIS_COMMENT block and it is not the first, + * the init function will reorder metadata by moving the VORBIS_COMMENT block to the front; the relative ordering of the other blocks will remain as they were. + * + * The Ogg FLAC mapping limits the number of metadata blocks per stream to 65535. If num_blocks exceeds this the function will return false. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @param {Flac.PointerInfo} metadataBuffersPointer + * + * @param {number} num_blocks + * + * @returns {boolean} false if the encoder is already initialized, else true. false if the encoder is already initialized, or if num_blocks > 65535 if encoding to Ogg FLAC, else true. + * + * @memberOf Flac# + * @function + * + * @see Flac.FLAC__MetadataType + * @see Flac#_create_pointer_array + * @see Flac#_destroy_pointer_array + */ + FLAC__stream_encoder_set_metadata: function(encoder, metadataBuffersPointer, num_blocks){// ( FLAC__StreamEncoder * encoder, FLAC__StreamMetadata ** metadata, unsigned num_blocks) + var status = Module.ccall('FLAC__stream_encoder_set_metadata', 'number', + ['number', 'number', 'number'], + [encoder, metadataBuffersPointer.pointerPointer, num_blocks] + ); + return status; + }, + /** + * Helper object for allocating an array of buffers on the (memory) heap. + * + * @interface PointerInfo + * @memberOf Flac + * @property {number} pointerPointer pointer to the array of (pointer) buffers + * @property {number[]} dataPointer array of pointers to the allocated data arrays (i.e. buffers) + * + * @see Flac#_create_pointer_array + * @see Flac#_destroy_pointer_array + */ + /** + * Helper function for creating pointer (and allocating the data) to an array of buffers on the (memory) heap. + * + * Use the returned PointerInfo.dataPointer as argument, where the array-pointer is required. + * + * NOTE: afer use, the allocated buffers on the heap need be freed, see {@link #_destroy_pointer_array}. + * + * @param {Uint8Array[]} bufferArray + * the buffer for which to create + * + * @returns {Flac.PointerInfo} false if the decoder is already initialized, else true + * + * @memberOf Flac# + * @function + * + * @see Flac#_destroy_pointer_array + */ + _create_pointer_array: function(bufferArray){ + var size=bufferArray.length; + var ptrs = [], ptrData = new Uint32Array(size); + var ptrOffsets = new DataView(ptrData.buffer); + var buffer, numBytes, heapBytes, ptr; + for(var i=0, size; i < size; ++i){ + buffer = bufferArray[i]; + // get the length of the data in bytes + numBytes = buffer.length * buffer.BYTES_PER_ELEMENT; + // malloc enough space for the data + ptr = Module._malloc(numBytes); + ptrs.push(ptr); + // get a bytes-wise view on the newly allocated buffer + heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); + // copy data into heapBytes + heapBytes.set(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));// use FIX for issue #11 (2) + ptrOffsets.setUint32(i*4, ptr, true); + } + var nPointerBytes = ptrData.length * ptrData.BYTES_PER_ELEMENT + var pointerPtr = Module._malloc(nPointerBytes); + var pointerHeap = new Uint8Array(Module.HEAPU8.buffer, pointerPtr, nPointerBytes); + pointerHeap.set( new Uint8Array(ptrData.buffer) ); + + return { + dataPointer: ptrs, + pointerPointer: pointerPtr + }; + }, + /** + * Helper function for destroying/freeing a previously created pointer (and allocating the data) of an array of buffers on the (memory) heap. + * + * @param {Flac.PointerInfo} pointerInfo + * the pointer / allocation information that should be destroyed/freed + * + * + * @memberOf Flac# + * @function + * + * @see Flac#_create_pointer_array + */ + _destroy_pointer_array: function(pointerInfo){ + var pointerArray = pointerInfo.dataPointer; + for(var i=0, size=pointerArray.length; i < size; ++i){ + Module._free(pointerArray[i]); + } + Module._free(pointerInfo.pointerPointer); + }, + /** + * Get if MD5 verification is enabled for the decoder + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} true if MD5 verification is enabled + * + * @memberOf Flac# + * @function + * + * @see #FLAC__stream_decoder_set_md5_checking + */ + FLAC__stream_decoder_get_md5_checking: Module.cwrap('FLAC__stream_decoder_get_md5_checking', 'number', ['number']), + + /** + * Set the "MD5 signature checking" flag. If true, the decoder will compute the MD5 signature of the unencoded audio data while decoding and compare it to the signature from the STREAMINFO block, + * if it exists, during {@link Flac.FLAC__stream_decoder_finish FLAC__stream_decoder_finish()}. + * + * MD5 signature checking will be turned off (until the next {@link Flac.FLAC__stream_decoder_reset FLAC__stream_decoder_reset()}) if there is no signature in the STREAMINFO block or when a seek is attempted. + * + * Clients that do not use the MD5 check should leave this off to speed up decoding. + * + * @param {number} decoder + * the ID of the decoder instance + * @param {boolean} is_verify + * enable/disable checksum verification during decoding + * @returns {boolean} FALSE if the decoder is already initialized, else TRUE. + * + * @memberOf Flac# + * @function + * + * @see #FLAC__stream_decoder_get_md5_checking + */ + FLAC__stream_decoder_set_md5_checking: function(decoder, is_verify){ + is_verify = is_verify? 1 : 0; + return Module.ccall('FLAC__stream_decoder_set_md5_checking', 'number', ['number', 'number'], [ decoder, is_verify ]); + }, + + /** + * Finish the encoding process. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @returns {boolean} false if an error occurred processing the last frame; + * or if verify mode is set, there was a verify mismatch; else true. + * If false, caller should check the state with {@link Flac#FLAC__stream_encoder_get_state} + * for more information about the error. + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_finish: Module.cwrap('FLAC__stream_encoder_finish', 'number', [ 'number' ]), + /** + * Finish the decoding process. + * + * The decoder can be reused, after initializing it again. + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} false if MD5 checking is on AND a STREAMINFO block was available AND the MD5 signature in + * the STREAMINFO block was non-zero AND the signature does not match the one computed by the decoder; + * else true. + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_finish: Module.cwrap('FLAC__stream_decoder_finish', 'number', [ 'number' ]), + /** + * Reset the decoder for reuse. + * + *

+ * NOTE: Needs to be re-initialized, before it can be used again + * + * @param {number} decoder + * the ID of the decoder instance + * + * @returns {boolean} true if successful + * + * @see #init_decoder_stream + * @see #init_decoder_ogg_stream + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_reset: Module.cwrap('FLAC__stream_decoder_reset', 'number', [ 'number' ]), + /** + * Delete the encoder instance, and free up its resources. + * + * @param {number} encoder + * the ID of the encoder instance + * + * @memberOf Flac# + * @function + */ + FLAC__stream_encoder_delete: function(encoder){ + this._clear_enc_cb(encoder);//<- remove callback references + Module.ccall('FLAC__stream_encoder_delete', 'number', [ 'number' ], [encoder]); + do_fire_event('destroyed', [{type: 'destroyed', target: {id: encoder, type: 'encoder'}}], false); + }, + /** + * Delete the decoder instance, and free up its resources. + * + * @param {number} decoder + * the ID of the decoder instance + * + * @memberOf Flac# + * @function + */ + FLAC__stream_decoder_delete: function(decoder){ + this._clear_dec_cb(decoder);//<- remove callback references + Module.ccall('FLAC__stream_decoder_delete', 'number', [ 'number' ], [decoder]); + do_fire_event('destroyed', [{type: 'destroyed', target: {id: decoder, type: 'decoder'}}], false); + } + +};//END: var _exported = { + +//if Properties are supported by JS execution environment: +// support "immediate triggering" onready function, if library is already initialized when setting onready callback +if(typeof Object.defineProperty === 'function'){ + //add internal field for storing onready callback: + _exported._onready = void(0); + //define getter & define setter with "immediate trigger" functionality: + Object.defineProperty(_exported, 'onready', { + get() { return this._onready; }, + set(newValue) { + this._onready = newValue; + if(newValue && this.isReady()){ + check_and_trigger_persisted_event('ready', newValue); + } + } + }); +} else { + //if Properties are NOTE supported by JS execution environment: + // pring usage warning for onready hook instead + console.warn('WARN: note that setting Flac.onready handler after Flac.isReady() is already true, will have no effect, that is, the handler function will not be triggered!'); +} + +if(expLib && expLib.exports){ + expLib.exports = _exported; +} +return _exported; + +}));//END: UMD wrapper diff --git a/music_assistant/providers/snapcast/snapweb/config.js b/music_assistant/providers/snapcast/snapweb/config.js new file mode 100644 index 00000000..39bc3c0b --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/config.js @@ -0,0 +1,5 @@ +"use strict"; +let config = { + baseUrl: (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host +}; +//# sourceMappingURL=config.js.map diff --git a/music_assistant/providers/snapcast/snapweb/favicon.ico b/music_assistant/providers/snapcast/snapweb/favicon.ico new file mode 100644 index 00000000..1ec3fa87 Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/favicon.ico differ diff --git a/music_assistant/providers/snapcast/snapweb/index.html b/music_assistant/providers/snapcast/snapweb/index.html new file mode 100644 index 00000000..1342e102 --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/index.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + Snapweb + + + + + + + +

+ + + diff --git a/music_assistant/providers/snapcast/snapweb/launcher-icon.png b/music_assistant/providers/snapcast/snapweb/launcher-icon.png new file mode 100644 index 00000000..8a005f12 Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/launcher-icon.png differ diff --git a/music_assistant/providers/snapcast/snapweb/manifest.json b/music_assistant/providers/snapcast/snapweb/manifest.json new file mode 100644 index 00000000..df145ff0 --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/manifest.json @@ -0,0 +1,16 @@ +{ + "short_name": "Snapweb", + "name": "Snapcast WebApp", + "icons": [ + { + "src": "launcher-icon.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "/index.html", + "display": "standalone", + "categories": ["music"], + "description": "Snapcast web client", + "theme_color": "#455A64" +} diff --git a/music_assistant/providers/snapcast/snapweb/mute_icon.png b/music_assistant/providers/snapcast/snapweb/mute_icon.png new file mode 100644 index 00000000..cf85867e Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/mute_icon.png differ diff --git a/music_assistant/providers/snapcast/snapweb/play.png b/music_assistant/providers/snapcast/snapweb/play.png new file mode 100644 index 00000000..41f76bbf Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/play.png differ diff --git a/music_assistant/providers/snapcast/snapweb/snapcast-512.png b/music_assistant/providers/snapcast/snapweb/snapcast-512.png new file mode 100644 index 00000000..96404404 Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/snapcast-512.png differ diff --git a/music_assistant/providers/snapcast/snapweb/snapcontrol.js b/music_assistant/providers/snapcast/snapweb/snapcontrol.js new file mode 100644 index 00000000..0003b929 --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/snapcontrol.js @@ -0,0 +1,903 @@ +"use strict"; +class Host { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.arch = json.arch; + this.ip = json.ip; + this.mac = json.mac; + this.name = json.name; + this.os = json.os; + } + arch = ""; + ip = ""; + mac = ""; + name = ""; + os = ""; +} +class Client { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.id = json.id; + this.host = new Host(json.host); + let jsnapclient = json.snapclient; + this.snapclient = { name: jsnapclient.name, protocolVersion: jsnapclient.protocolVersion, version: jsnapclient.version }; + let jconfig = json.config; + this.config = { instance: jconfig.instance, latency: jconfig.latency, name: jconfig.name, volume: { muted: jconfig.volume.muted, percent: jconfig.volume.percent } }; + this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec }; + this.connected = Boolean(json.connected); + } + id = ""; + host; + snapclient; + config; + lastSeen; + connected = false; +} +class Group { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.name = json.name; + this.id = json.id; + this.stream_id = json.stream_id; + this.muted = Boolean(json.muted); + for (let client of json.clients) + this.clients.push(new Client(client)); + } + name = ""; + id = ""; + stream_id = ""; + muted = false; + clients = []; + getClient(id) { + for (let client of this.clients) { + if (client.id == id) + return client; + } + return null; + } +} +class Metadata { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.title = json.title; + this.artist = json.artist; + this.album = json.album; + this.artUrl = json.artUrl; + this.duration = json.duration; + } + title; + artist; + album; + artUrl; + duration; +} +class Properties { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.loopStatus = json.loopStatus; + this.shuffle = json.shuffle; + this.volume = json.volume; + this.rate = json.rate; + this.playbackStatus = json.playbackStatus; + this.position = json.position; + this.minimumRate = json.minimumRate; + this.maximumRate = json.maximumRate; + this.canGoNext = Boolean(json.canGoNext); + this.canGoPrevious = Boolean(json.canGoPrevious); + this.canPlay = Boolean(json.canPlay); + this.canPause = Boolean(json.canPause); + this.canSeek = Boolean(json.canSeek); + this.canControl = Boolean(json.canControl); + if (json.metadata != undefined) { + this.metadata = new Metadata(json.metadata); + } + else { + this.metadata = new Metadata({}); + } + } + loopStatus; + shuffle; + volume; + rate; + playbackStatus; + position; + minimumRate; + maximumRate; + canGoNext = false; + canGoPrevious = false; + canPlay = false; + canPause = false; + canSeek = false; + canControl = false; + metadata; +} +class Stream { + constructor(json) { + this.fromJson(json); + } + fromJson(json) { + this.id = json.id; + this.status = json.status; + if (json.properties != undefined) { + this.properties = new Properties(json.properties); + } + else { + this.properties = new Properties({}); + } + let juri = json.uri; + this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query }; + } + id = ""; + status = ""; + uri; + properties; +} +class Server { + constructor(json) { + if (json) + this.fromJson(json); + } + fromJson(json) { + this.groups = []; + for (let jgroup of json.groups) + this.groups.push(new Group(jgroup)); + let jsnapserver = json.server.snapserver; + this.server = { host: new Host(json.server.host), snapserver: { controlProtocolVersion: jsnapserver.controlProtocolVersion, name: jsnapserver.name, protocolVersion: jsnapserver.protocolVersion, version: jsnapserver.version } }; + this.streams = []; + for (let jstream of json.streams) { + this.streams.push(new Stream(jstream)); + } + } + groups = []; + server; + streams = []; + getClient(id) { + for (let group of this.groups) { + let client = group.getClient(id); + if (client) + return client; + } + return null; + } + getGroup(id) { + for (let group of this.groups) { + if (group.id == id) + return group; + } + return null; + } + getStream(id) { + for (let stream of this.streams) { + if (stream.id == id) + return stream; + } + return null; + } +} +class SnapControl { + constructor(baseUrl) { + this.server = new Server(); + this.baseUrl = baseUrl; + this.msg_id = 0; + this.status_req_id = -1; + this.connect(); + } + connect() { + this.connection = new WebSocket(this.baseUrl + '/jsonrpc'); + this.connection.onmessage = (msg) => this.onMessage(msg.data); + this.connection.onopen = () => { this.status_req_id = this.sendRequest('Server.GetStatus'); }; + this.connection.onerror = (ev) => { console.error('error:', ev); }; + this.connection.onclose = () => { + console.info('connection lost, reconnecting in 1s'); + setTimeout(() => this.connect(), 1000); + }; + } + onNotification(notification) { + let stream; + switch (notification.method) { + case 'Client.OnVolumeChanged': + let client = this.getClient(notification.params.id); + client.config.volume = notification.params.volume; + updateGroupVolume(this.getGroupFromClient(client.id)); + return true; + case 'Client.OnLatencyChanged': + this.getClient(notification.params.id).config.latency = notification.params.latency; + return false; + case 'Client.OnNameChanged': + this.getClient(notification.params.id).config.name = notification.params.name; + return true; + case 'Client.OnConnect': + case 'Client.OnDisconnect': + this.getClient(notification.params.client.id).fromJson(notification.params.client); + return true; + case 'Group.OnMute': + this.getGroup(notification.params.id).muted = Boolean(notification.params.mute); + return true; + case 'Group.OnStreamChanged': + this.getGroup(notification.params.id).stream_id = notification.params.stream_id; + this.updateProperties(notification.params.stream_id); + return true; + case 'Stream.OnUpdate': + stream = this.getStream(notification.params.id); + stream.fromJson(notification.params.stream); + this.updateProperties(stream.id); + return true; + case 'Server.OnUpdate': + this.server.fromJson(notification.params.server); + this.updateProperties(this.getMyStreamId()); + return true; + case 'Stream.OnProperties': + stream = this.getStream(notification.params.id); + stream.properties.fromJson(notification.params.properties); + if (this.getMyStreamId() == stream.id) + this.updateProperties(stream.id); + return false; + default: + return false; + } + } + updateProperties(stream_id) { + if (!('mediaSession' in navigator)) { + console.log('updateProperties: mediaSession not supported'); + return; + } + if (stream_id != this.getMyStreamId()) { + console.log('updateProperties: not my stream id: ' + stream_id + ', mine: ' + this.getMyStreamId()); + return; + } + let props; + let metadata; + try { + props = this.getStreamFromClient(SnapStream.getClientId()).properties; + metadata = this.getStreamFromClient(SnapStream.getClientId()).properties.metadata; + } + catch (e) { + console.log('updateProperties failed: ' + e); + return; + } + // https://developers.google.com/web/updates/2017/02/media-session + // https://github.com/googlechrome/samples/tree/gh-pages/media-session + // https://googlechrome.github.io/samples/media-session/audio.html + // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession/setActionHandler#seekto + console.log('updateProperties: ', props); + let play_state = "none"; + if (props.playbackStatus != undefined) { + if (props.playbackStatus == "playing") { + audio.play(); + play_state = "playing"; + } + else if (props.playbackStatus == "paused") { + audio.pause(); + play_state = "paused"; + } + else if (props.playbackStatus == "stopped") { + audio.pause(); + play_state = "none"; + } + } + let mediaSession = navigator.mediaSession; + mediaSession.playbackState = play_state; + console.log('updateProperties playbackState: ', navigator.mediaSession.playbackState); + // if (props.canGoNext == undefined || !props.canGoNext!) + mediaSession.setActionHandler('play', () => { + props.canPlay ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'play' }) : null; + }); + mediaSession.setActionHandler('pause', () => { + props.canPause ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'pause' }) : null; + }); + mediaSession.setActionHandler('previoustrack', () => { + props.canGoPrevious ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'previous' }) : null; + }); + mediaSession.setActionHandler('nexttrack', () => { + props.canGoNext ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'next' }) : null; + }); + try { + mediaSession.setActionHandler('stop', () => { + props.canControl ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'stop' }) : null; + }); + } + catch (error) { + console.log('Warning! The "stop" media session action is not supported.'); + } + let defaultSkipTime = 10; // Time to skip in seconds by default + mediaSession.setActionHandler('seekbackward', (event) => { + let offset = (event.seekOffset || defaultSkipTime) * -1; + if (props.position != undefined) + Math.max(props.position + offset, 0); + props.canSeek ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null; + }); + mediaSession.setActionHandler('seekforward', (event) => { + let offset = event.seekOffset || defaultSkipTime; + if ((metadata.duration != undefined) && (props.position != undefined)) + Math.min(props.position + offset, metadata.duration); + props.canSeek ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null; + }); + try { + mediaSession.setActionHandler('seekto', (event) => { + let position = event.seekTime || 0; + if (metadata.duration != undefined) + Math.min(position, metadata.duration); + props.canSeek ? + this.sendRequest('Stream.Control', { id: stream_id, command: 'setPosition', params: { 'position': position } }) : null; + }); + } + catch (error) { + console.log('Warning! The "seekto" media session action is not supported.'); + } + if ((metadata.duration != undefined) && (props.position != undefined) && (props.position <= metadata.duration)) { + if ('setPositionState' in mediaSession) { + console.log('Updating position state: ' + props.position + '/' + metadata.duration); + mediaSession.setPositionState({ + duration: metadata.duration, + playbackRate: 1.0, + position: props.position + }); + } + } + else { + mediaSession.setPositionState({ + duration: 0, + playbackRate: 1.0, + position: 0 + }); + } + console.log('updateMetadata: ', metadata); + // https://github.com/Microsoft/TypeScript/issues/19473 + let title = metadata.title || "Unknown Title"; + let artist = (metadata.artist != undefined) ? metadata.artist.join(', ') : "Unknown Artist"; + let album = metadata.album || ""; + let artwork = [{ src: 'snapcast-512.png', sizes: '512x512', type: 'image/png' }]; + if (metadata.artUrl != undefined) { + artwork = [ + { src: metadata.artUrl, sizes: '96x96', type: 'image/png' }, + { src: metadata.artUrl, sizes: '128x128', type: 'image/png' }, + { src: metadata.artUrl, sizes: '192x192', type: 'image/png' }, + { src: metadata.artUrl, sizes: '256x256', type: 'image/png' }, + { src: metadata.artUrl, sizes: '384x384', type: 'image/png' }, + { src: metadata.artUrl, sizes: '512x512', type: 'image/png' }, + ]; + } // || 'snapcast-512.png'; + console.log('Metadata title: ' + title + ', artist: ' + artist + ', album: ' + album + ", artwork: " + artwork); + navigator.mediaSession.metadata = new MediaMetadata({ + title: title, + artist: artist, + album: album, + artwork: artwork + }); + // mediaSession.setActionHandler('seekbackward', function () { }); + // mediaSession.setActionHandler('seekforward', function () { }); + } + getClient(client_id) { + let client = this.server.getClient(client_id); + if (client == null) { + throw new Error(`client ${client_id} was null`); + } + return client; + } + getGroup(group_id) { + let group = this.server.getGroup(group_id); + if (group == null) { + throw new Error(`group ${group_id} was null`); + } + return group; + } + getGroupVolume(group, online) { + if (group.clients.length == 0) + return 0; + let group_vol = 0; + let client_count = 0; + for (let client of group.clients) { + if (online && !client.connected) + continue; + group_vol += client.config.volume.percent; + ++client_count; + } + if (client_count == 0) + return 0; + return group_vol / client_count; + } + getGroupFromClient(client_id) { + for (let group of this.server.groups) + for (let client of group.clients) + if (client.id == client_id) + return group; + throw new Error(`group for client ${client_id} was null`); + } + getStreamFromClient(client_id) { + let group = this.getGroupFromClient(client_id); + return this.getStream(group.stream_id); + } + getMyStreamId() { + try { + let group = this.getGroupFromClient(SnapStream.getClientId()); + return this.getStream(group.stream_id).id; + } + catch (e) { + return ""; + } + } + getStream(stream_id) { + let stream = this.server.getStream(stream_id); + if (stream == null) { + throw new Error(`stream ${stream_id} was null`); + } + return stream; + } + setVolume(client_id, percent, mute) { + percent = Math.max(0, Math.min(100, percent)); + let client = this.getClient(client_id); + client.config.volume.percent = percent; + if (mute != undefined) + client.config.volume.muted = mute; + this.sendRequest('Client.SetVolume', { id: client_id, volume: { muted: client.config.volume.muted, percent: client.config.volume.percent } }); + } + setClientName(client_id, name) { + let client = this.getClient(client_id); + let current_name = (client.config.name != "") ? client.config.name : client.host.name; + if (name != current_name) { + this.sendRequest('Client.SetName', { id: client_id, name: name }); + client.config.name = name; + } + } + setClientLatency(client_id, latency) { + let client = this.getClient(client_id); + let current_latency = client.config.latency; + if (latency != current_latency) { + this.sendRequest('Client.SetLatency', { id: client_id, latency: latency }); + client.config.latency = latency; + } + } + deleteClient(client_id) { + this.sendRequest('Server.DeleteClient', { id: client_id }); + this.server.groups.forEach((g, gi) => { + g.clients.forEach((c, ci) => { + if (c.id == client_id) { + this.server.groups[gi].clients.splice(ci, 1); + } + }); + }); + this.server.groups.forEach((g, gi) => { + if (g.clients.length == 0) { + this.server.groups.splice(gi, 1); + } + }); + show(); + } + setStream(group_id, stream_id) { + this.getGroup(group_id).stream_id = stream_id; + this.updateProperties(stream_id); + this.sendRequest('Group.SetStream', { id: group_id, stream_id: stream_id }); + } + setClients(group_id, clients) { + this.status_req_id = this.sendRequest('Group.SetClients', { id: group_id, clients: clients }); + } + muteGroup(group_id, mute) { + this.getGroup(group_id).muted = mute; + this.sendRequest('Group.SetMute', { id: group_id, mute: mute }); + } + sendRequest(method, params) { + let msg = { + id: ++this.msg_id, + jsonrpc: '2.0', + method: method + }; + if (params) + msg.params = params; + let msgJson = JSON.stringify(msg); + console.log("Sending: " + msgJson); + this.connection.send(msgJson); + return this.msg_id; + } + onMessage(msg) { + let json_msg = JSON.parse(msg); + let is_response = (json_msg.id != undefined); + console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(json_msg)); + if (is_response) { + if (json_msg.id == this.status_req_id) { + this.server = new Server(json_msg.result.server); + this.updateProperties(this.getMyStreamId()); + show(); + } + } + else { + let refresh = false; + if (Array.isArray(json_msg)) { + for (let notification of json_msg) { + refresh = this.onNotification(notification) || refresh; + } + } + else { + refresh = this.onNotification(json_msg); + } + // TODO: don't update everything, but only the changed, + // e.g. update the values for the volume sliders + if (refresh) + show(); + } + } + baseUrl; + connection; + server; + msg_id; + status_req_id; +} +let snapcontrol; +let snapstream = null; +let hide_offline = true; +let autoplay_done = false; +let audio = document.createElement('audio'); +function autoplayRequested() { + return document.location.hash.match(/autoplay/) !== null; +} +function show() { + // Render the page + const versionElem = document.getElementsByTagName("meta").namedItem("version"); + console.log("Snapweb version " + (versionElem ? versionElem.content : "null")); + let play_img; + if (snapstream) { + play_img = 'stop.png'; + } + else { + play_img = 'play.png'; + } + let content = ""; + content += ""; + content += "
"; + let server = snapcontrol.server; + for (let group of server.groups) { + if (hide_offline) { + let groupActive = false; + for (let client of group.clients) { + if (client.connected) { + groupActive = true; + break; + } + } + if (!groupActive) + continue; + } + // Set mute variables + let classgroup; + let muted; + let mute_img; + if (group.muted == true) { + classgroup = 'group muted'; + muted = true; + mute_img = 'mute_icon.png'; + } + else { + classgroup = 'group'; + muted = false; + mute_img = 'speaker_icon.png'; + } + // Start group div + content += "
"; + // Create stream selection dropdown + let streamselect = ""; + // Group mute and refresh button + content += "
"; + content += streamselect; + // let cover_img: string = server.getStream(group.stream_id)!.properties.metadata.artUrl || "snapcast-512.png"; + // content += ""; + let clientCount = 0; + for (let client of group.clients) + if (!hide_offline || client.connected) + clientCount++; + if (clientCount > 1) { + let volume = snapcontrol.getGroupVolume(group, hide_offline); + // content += "
"; + content += ""; + content += "
"; + content += " "; + // content += " "; + content += "
"; + // content += "
"; + } + // transparent placeholder edit icon + content += "
"; + content += "
"; + content += "
"; + // Create clients in group + for (let client of group.clients) { + if (!client.connected && hide_offline) + continue; + // Set name and connection state vars, start client div + let name; + let clas = 'client'; + if (client.config.name != "") { + name = client.config.name; + } + else { + name = client.host.name; + } + if (client.connected == false) { + clas = 'client disconnected'; + } + content += "
"; + // Client mute status vars + let muted; + let mute_img; + let sliderclass; + if (client.config.volume.muted == true) { + muted = true; + sliderclass = 'slider muted'; + mute_img = 'mute_icon.png'; + } + else { + sliderclass = 'slider'; + muted = false; + mute_img = 'speaker_icon.png'; + } + // Populate client div + content += ""; + content += "
"; + content += " "; + content += "
"; + content += " "; + content += " "; + if (client.connected == false) { + content += " 🗑"; + content += " "; + } + else { + content += ""; + } + content += "
" + name + "
"; + content += "
"; + } + content += "
"; + } + content += "
"; // content + content += "
"; + content += "
"; + content += "
"; + content += " "; + content += " "; + content += " "; + content += " "; + content += " "; + content += " "; + content += " "; + content += "
"; + content += "
"; + content += "
"; + // Pad then update page + content = content + "

"; + document.getElementById('show').innerHTML = content; + let playElem = document.getElementById('play-button'); + playElem.onclick = () => { + play(); + }; + for (let group of snapcontrol.server.groups) { + if (group.clients.length > 1) { + let slider = document.getElementById("vol_" + group.id); + if (slider == null) + continue; + slider.addEventListener('pointerdown', function () { + groupVolumeEnter(group.id); + }); + slider.addEventListener('touchstart', function () { + groupVolumeEnter(group.id); + }); + } + } +} +function updateGroupVolume(group) { + let group_vol = snapcontrol.getGroupVolume(group, hide_offline); + let slider = document.getElementById("vol_" + group.id); + if (slider == null) + return; + console.log("updateGroupVolume group: " + group.id + ", volume: " + group_vol + ", slider: " + (slider != null)); + slider.value = String(group_vol); +} +let client_volumes; +let group_volume; +function setGroupVolume(group_id) { + let group = snapcontrol.getGroup(group_id); + let percent = document.getElementById('vol_' + group.id).valueAsNumber; + console.log("setGroupVolume id: " + group.id + ", volume: " + percent); + // show() + let delta = percent - group_volume; + let ratio; + if (delta < 0) + ratio = (group_volume - percent) / group_volume; + else + ratio = (percent - group_volume) / (100 - group_volume); + for (let i = 0; i < group.clients.length; ++i) { + let new_volume = client_volumes[i]; + if (delta < 0) + new_volume -= ratio * client_volumes[i]; + else + new_volume += ratio * (100 - client_volumes[i]); + let client_id = group.clients[i].id; + // TODO: use batch request to update all client volumes at once + snapcontrol.setVolume(client_id, new_volume); + let slider = document.getElementById('vol_' + client_id); + if (slider) + slider.value = String(new_volume); + } +} +function groupVolumeEnter(group_id) { + let group = snapcontrol.getGroup(group_id); + let percent = document.getElementById('vol_' + group.id).valueAsNumber; + console.log("groupVolumeEnter id: " + group.id + ", volume: " + percent); + group_volume = percent; + client_volumes = []; + for (let i = 0; i < group.clients.length; ++i) { + client_volumes.push(group.clients[i].config.volume.percent); + } + // show() +} +function setVolume(id, mute) { + console.log("setVolume id: " + id + ", mute: " + mute); + let percent = document.getElementById('vol_' + id).valueAsNumber; + let client = snapcontrol.getClient(id); + let needs_update = (mute != client.config.volume.muted); + snapcontrol.setVolume(id, percent, mute); + let group = snapcontrol.getGroupFromClient(id); + updateGroupVolume(group); + if (needs_update) + show(); +} +function play() { + if (snapstream) { + snapstream.stop(); + snapstream = null; + audio.pause(); + audio.src = ''; + document.body.removeChild(audio); + } + else { + snapstream = new SnapStream(config.baseUrl); + // User interacted with the page. Let's play audio... + document.body.appendChild(audio); + audio.src = "10-seconds-of-silence.mp3"; + audio.loop = true; + audio.play().then(() => { + snapcontrol.updateProperties(snapcontrol.getMyStreamId()); + }); + } +} +function setMuteGroup(id, mute) { + snapcontrol.muteGroup(id, mute); + show(); +} +function setStream(id) { + snapcontrol.setStream(id, document.getElementById('stream_' + id).value); + show(); +} +function setGroup(client_id, group_id) { + console.log("setGroup id: " + client_id + ", group: " + group_id); + let server = snapcontrol.server; + // Get client group id + let current_group = snapcontrol.getGroupFromClient(client_id); + // Get + // List of target group's clients + // OR + // List of current group's other clients + let send_clients = []; + for (let i_group = 0; i_group < server.groups.length; i_group++) { + if (server.groups[i_group].id == group_id || (group_id == "new" && server.groups[i_group].id == current_group.id)) { + for (let i_client = 0; i_client < server.groups[i_group].clients.length; i_client++) { + if (group_id == "new" && server.groups[i_group].clients[i_client].id == client_id) { } + else { + send_clients[send_clients.length] = server.groups[i_group].clients[i_client].id; + } + } + } + } + if (group_id == "new") + group_id = current_group.id; + else + send_clients[send_clients.length] = client_id; + snapcontrol.setClients(group_id, send_clients); +} +function setName(id) { + // Get current name and lacency + let client = snapcontrol.getClient(id); + let current_name = (client.config.name != "") ? client.config.name : client.host.name; + let current_latency = client.config.latency; + let new_name = window.prompt("New Name", current_name); + let new_latency = Number(window.prompt("New Latency", String(current_latency))); + if (new_name != null) + snapcontrol.setClientName(id, new_name); + if (new_latency != null) + snapcontrol.setClientLatency(id, new_latency); + show(); +} +function openClientSettings(id) { + let modal = document.getElementById("client-settings"); + let client = snapcontrol.getClient(id); + let current_name = (client.config.name != "") ? client.config.name : client.host.name; + let name = document.getElementById("client-name"); + name.name = id; + name.value = current_name; + let latency = document.getElementById("client-latency"); + latency.valueAsNumber = client.config.latency; + let group = snapcontrol.getGroupFromClient(id); + let group_input = document.getElementById("client-group"); + while (group_input.length > 0) + group_input.remove(0); + let group_num = 0; + for (let ogroup of snapcontrol.server.groups) { + let option = document.createElement('option'); + option.value = ogroup.id; + option.text = "Group " + (group_num + 1) + " (" + ogroup.clients.length + " Clients)"; + group_input.add(option); + if (ogroup == group) { + console.log("Selected: " + group_num); + group_input.selectedIndex = group_num; + } + ++group_num; + } + let option = document.createElement('option'); + option.value = option.text = "new"; + group_input.add(option); + modal.style.display = "block"; +} +function closeClientSettings() { + let name = document.getElementById("client-name"); + let id = name.name; + console.log("onclose " + id + ", value: " + name.value); + snapcontrol.setClientName(id, name.value); + let latency = document.getElementById("client-latency"); + snapcontrol.setClientLatency(id, latency.valueAsNumber); + let group_input = document.getElementById("client-group"); + let option = group_input.options[group_input.selectedIndex]; + setGroup(id, option.value); + let modal = document.getElementById("client-settings"); + modal.style.display = "none"; + show(); +} +function deleteClient(id) { + if (confirm('Are you sure?')) { + snapcontrol.deleteClient(id); + } +} +window.onload = function () { + snapcontrol = new SnapControl(config.baseUrl); +}; +// When the user clicks anywhere outside of the modal, close it +window.onclick = function (event) { + let modal = document.getElementById("client-settings"); + if (event.target == modal) { + modal.style.display = "none"; + } +}; +//# sourceMappingURL=snapcontrol.js.map diff --git a/music_assistant/providers/snapcast/snapweb/snapstream.js b/music_assistant/providers/snapcast/snapweb/snapstream.js new file mode 100644 index 00000000..b431c137 --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/snapstream.js @@ -0,0 +1,919 @@ +"use strict"; +function setCookie(key, value, exdays = -1) { + let d = new Date(); + if (exdays < 0) + exdays = 10 * 365; + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + let expires = "expires=" + d.toUTCString(); + document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/"; +} +function getPersistentValue(key, defaultValue = "") { + if (!!window.localStorage) { + const value = window.localStorage.getItem(key); + if (value !== null) { + return value; + } + window.localStorage.setItem(key, defaultValue); + return defaultValue; + } + // Fallback to cookies if localStorage is not available. + let name = key + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(';'); + for (let c of ca) { + c = c.trimLeft(); + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + setCookie(key, defaultValue); + return defaultValue; +} +function getChromeVersion() { + const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + return raw ? parseInt(raw[2]) : null; +} +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +class Tv { + constructor(sec, usec) { + this.sec = sec; + this.usec = usec; + } + setMilliseconds(ms) { + this.sec = Math.floor(ms / 1000); + this.usec = Math.floor(ms * 1000) % 1000000; + } + getMilliseconds() { + return this.sec * 1000 + this.usec / 1000; + } + sec = 0; + usec = 0; +} +class BaseMessage { + constructor(_buffer) { + } + deserialize(buffer) { + let view = new DataView(buffer); + this.type = view.getUint16(0, true); + this.id = view.getUint16(2, true); + this.refersTo = view.getUint16(4, true); + this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true)); + this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true)); + this.size = view.getUint32(22, true); + } + serialize() { + this.size = 26 + this.getSize(); + let buffer = new ArrayBuffer(this.size); + let view = new DataView(buffer); + view.setUint16(0, this.type, true); + view.setUint16(2, this.id, true); + view.setUint16(4, this.refersTo, true); + view.setInt32(6, this.sent.sec, true); + view.setInt32(10, this.sent.usec, true); + view.setInt32(14, this.received.sec, true); + view.setInt32(18, this.received.usec, true); + view.setUint32(22, this.size, true); + return buffer; + } + getSize() { + return 0; + } + type = 0; + id = 0; + refersTo = 0; + received = new Tv(0, 0); + sent = new Tv(0, 0); + size = 0; +} +class CodecMessage extends BaseMessage { + constructor(buffer) { + super(buffer); + this.payload = new ArrayBuffer(0); + if (buffer) { + this.deserialize(buffer); + } + this.type = 1; + } + deserialize(buffer) { + super.deserialize(buffer); + let view = new DataView(buffer); + let codecSize = view.getInt32(26, true); + let decoder = new TextDecoder("utf-8"); + this.codec = decoder.decode(buffer.slice(30, 30 + codecSize)); + let payloadSize = view.getInt32(30 + codecSize, true); + console.log("payload size: " + payloadSize); + this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize); + console.log("payload: " + this.payload); + } + codec = ""; + payload; +} +class TimeMessage extends BaseMessage { + constructor(buffer) { + super(buffer); + if (buffer) { + this.deserialize(buffer); + } + this.type = 4; + } + deserialize(buffer) { + super.deserialize(buffer); + let view = new DataView(buffer); + this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true)); + } + serialize() { + let buffer = super.serialize(); + let view = new DataView(buffer); + view.setInt32(26, this.latency.sec, true); + view.setInt32(30, this.latency.usec, true); + return buffer; + } + getSize() { + return 8; + } + latency = new Tv(0, 0); +} +class JsonMessage extends BaseMessage { + constructor(buffer) { + super(buffer); + if (buffer) { + this.deserialize(buffer); + } + } + deserialize(buffer) { + super.deserialize(buffer); + let view = new DataView(buffer); + let size = view.getUint32(26, true); + let decoder = new TextDecoder(); + this.json = JSON.parse(decoder.decode(buffer.slice(30, 30 + size))); + } + serialize() { + let buffer = super.serialize(); + let view = new DataView(buffer); + let jsonStr = JSON.stringify(this.json); + view.setUint32(26, jsonStr.length, true); + let encoder = new TextEncoder(); + let encoded = encoder.encode(jsonStr); + for (let i = 0; i < encoded.length; ++i) + view.setUint8(30 + i, encoded[i]); + return buffer; + } + getSize() { + let encoder = new TextEncoder(); + let encoded = encoder.encode(JSON.stringify(this.json)); + return encoded.length + 4; + // return JSON.stringify(this.json).length; + } + json; +} +class HelloMessage extends JsonMessage { + constructor(buffer) { + super(buffer); + if (buffer) { + this.deserialize(buffer); + } + this.type = 5; + } + deserialize(buffer) { + super.deserialize(buffer); + this.mac = this.json["MAC"]; + this.hostname = this.json["HostName"]; + this.version = this.json["Version"]; + this.clientName = this.json["ClientName"]; + this.os = this.json["OS"]; + this.arch = this.json["Arch"]; + this.instance = this.json["Instance"]; + this.uniqueId = this.json["ID"]; + this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"]; + } + serialize() { + this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion }; + return super.serialize(); + } + mac = ""; + hostname = ""; + version = "0.0.0"; + clientName = "Snapweb"; + os = ""; + arch = "web"; + instance = 1; + uniqueId = ""; + snapStreamProtocolVersion = 2; +} +class ServerSettingsMessage extends JsonMessage { + constructor(buffer) { + super(buffer); + if (buffer) { + this.deserialize(buffer); + } + this.type = 3; + } + deserialize(buffer) { + super.deserialize(buffer); + this.bufferMs = this.json["bufferMs"]; + this.latency = this.json["latency"]; + this.volumePercent = this.json["volume"]; + this.muted = this.json["muted"]; + } + serialize() { + this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted }; + return super.serialize(); + } + bufferMs = 0; + latency = 0; + volumePercent = 0; + muted = false; +} +class PcmChunkMessage extends BaseMessage { + constructor(buffer, sampleFormat) { + super(buffer); + this.deserialize(buffer); + this.sampleFormat = sampleFormat; + this.type = 2; + } + deserialize(buffer) { + super.deserialize(buffer); + let view = new DataView(buffer); + this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true)); + // this.payloadSize = view.getUint32(34, true); + this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize); + // console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength); + } + readFrames(frames) { + let frameCnt = frames; + let frameSize = this.sampleFormat.frameSize(); + if (this.idx + frames > this.payloadSize() / frameSize) + frameCnt = (this.payloadSize() / frameSize) - this.idx; + let begin = this.idx * frameSize; + this.idx += frameCnt; + let end = begin + frameCnt * frameSize; + // console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength); + return this.payload.slice(begin, end); + } + getFrameCount() { + return (this.payloadSize() / this.sampleFormat.frameSize()); + } + isEndOfChunk() { + return this.idx >= this.getFrameCount(); + } + startMs() { + return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate); + } + duration() { + return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate); + } + payloadSize() { + return this.payload.byteLength; + } + clearPayload() { + this.payload = new ArrayBuffer(0); + } + addPayload(buffer) { + let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength); + let view = new DataView(payload); + let viewOld = new DataView(this.payload); + let viewNew = new DataView(buffer); + for (let i = 0; i < viewOld.byteLength; ++i) { + view.setInt8(i, viewOld.getInt8(i)); + } + for (let i = 0; i < viewNew.byteLength; ++i) { + view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i)); + } + this.payload = payload; + } + timestamp = new Tv(0, 0); + // payloadSize: number = 0; + payload = new ArrayBuffer(0); + idx = 0; + sampleFormat; +} +class AudioStream { + timeProvider; + sampleFormat; + bufferMs; + constructor(timeProvider, sampleFormat, bufferMs) { + this.timeProvider = timeProvider; + this.sampleFormat = sampleFormat; + this.bufferMs = bufferMs; + } + chunks = new Array(); + setVolume(percent, muted) { + // let base = 10; + this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1); + console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted); + this.muted = muted; + } + addChunk(chunk) { + this.chunks.push(chunk); + // let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds(); + // let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds(); + // console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2)); + while (this.chunks.length > 0) { + let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds(); + // todo: consider buffer ms + if (age > 5000 + this.bufferMs) { + this.chunks.shift(); + console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length); + } + else + break; + } + } + getNextBuffer(buffer, playTimeMs) { + if (!this.chunk) { + this.chunk = this.chunks.shift(); + } + // let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs; + let frames = buffer.length; + // console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2)); + let left = new Float32Array(frames); + let right = new Float32Array(frames); + let read = 0; + let pos = 0; + // let volume = this.muted ? 0 : this.volume; + let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs); + if (this.chunk) { + let age = serverPlayTimeMs - this.chunk.startMs(); // - 500; + let reqChunkDuration = frames / this.sampleFormat.msRate(); + let secs = Math.floor(Date.now() / 1000); + if (this.lastLog != secs) { + this.lastLog = secs; + console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration); + } + if (age < -reqChunkDuration) { + console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2)); + console.log("Chunk too young, returning silence"); + } + else { + if (Math.abs(age) > 5) { + // We are 5ms apart, do a hard sync, i.e. don't play faster/slower, + // but seek to the desired position instead + while (this.chunk && age > this.chunk.duration()) { + console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")"); + this.chunk = this.chunks.shift(); + if (!this.chunk) + break; + age = serverPlayTimeMs - this.chunk.startMs(); + } + if (this.chunk) { + if (age > 0) { + console.log("Fast forwarding " + age.toFixed(2) + "ms"); + this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate())); + } + else if (age < 0) { + console.log("Playing silence " + -age.toFixed(2) + "ms"); + let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate()); + left.fill(0, 0, silentFrames); + right.fill(0, 0, silentFrames); + read = silentFrames; + pos = silentFrames; + } + age = 0; + } + } + // else if (age > 0.1) { + // let rate = age * 0.0005; + // rate = 1.0 - Math.min(rate, 0.0005); + // console.debug("Age > 0, rate: " + rate); + // // we are late (age > 0), this means we are not playing fast enough + // // => the real sample rate seems to be lower, we have to drop some frames + // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999); + // } + // else if (age < -0.1) { + // let rate = -age * 0.0005; + // rate = 1.0 + Math.min(rate, 0.0005); + // console.debug("Age < 0, rate: " + rate); + // // we are early (age > 0), this means we are playing too fast + // // => the real sample rate seems to be higher, we have to insert some frames + // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999); + // } + // else { + // this.setRealSampleRate(this.sampleFormat.rate); + // } + let addFrames = 0; + let everyN = 0; + if (age > 0.1) { + addFrames = Math.ceil(age); // / 5); + } + else if (age < -0.1) { + addFrames = Math.floor(age); // / 5); + } + // addFrames = -2; + let readFrames = frames + addFrames - read; + if (addFrames != 0) + everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1)); + // addFrames = 0; + // console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN); + while ((read < readFrames) && this.chunk) { + let pcmChunk = this.chunk; + let pcmBuffer = pcmChunk.readFrames(readFrames - read); + let payload = new Int16Array(pcmBuffer); + // console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length); + // read += (pcmBuffer.byteLength / this.sampleFormat.frameSize()); + for (let i = 0; i < payload.length; i += 2) { + read++; + left[pos] = (payload[i] / 32768); // * volume; + right[pos] = (payload[i + 1] / 32768); // * volume; + if ((everyN != 0) && (read % everyN == 0)) { + if (addFrames > 0) { + pos--; + } + else { + left[pos + 1] = left[pos]; + right[pos + 1] = right[pos]; + pos++; + // console.log("Add: " + pos); + } + } + pos++; + } + if (pcmChunk.isEndOfChunk()) { + this.chunk = this.chunks.shift(); + } + } + if (addFrames != 0) + console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN); + if (read == readFrames) + read = frames; + } + } + if (read < frames) { + console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length); + left.fill(0, pos); + right.fill(0, pos); + } + // copyToChannel is not supported by Safari + buffer.getChannelData(0).set(left); + buffer.getChannelData(1).set(right); + } + // setRealSampleRate(sampleRate: number) { + // if (sampleRate == this.sampleFormat.rate) { + // this.correctAfterXFrames = 0; + // } + // else { + // this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.)); + // console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames); + // } + // } + chunk = undefined; + volume = 1; + muted = false; + lastLog = 0; +} +class TimeProvider { + constructor(ctx = undefined) { + if (ctx) { + this.setAudioContext(ctx); + } + } + setAudioContext(ctx) { + this.ctx = ctx; + this.reset(); + } + reset() { + this.diffBuffer.length = 0; + this.diff = 0; + } + setDiff(c2s, s2c) { + if (this.now() == 0) { + this.reset(); + } + else { + if (this.diffBuffer.push((c2s - s2c) / 2) > 100) + this.diffBuffer.shift(); + let sorted = [...this.diffBuffer]; + sorted.sort(); + this.diff = sorted[Math.floor(sorted.length / 2)]; + } + // console.debug("c2s: " + c2s.toFixed(2) + ", s2c: " + s2c.toFixed(2) + ", diff: " + this.diff.toFixed(2) + ", now: " + this.now().toFixed(2) + ", server.now: " + this.serverNow().toFixed(2) + ", win.now: " + window.performance.now().toFixed(2)); + // console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now()); + } + now() { + if (!this.ctx) { + return window.performance.now(); + } + else { + // Use the more accurate getOutputTimestamp if available, fallback to ctx.currentTime otherwise. + const contextTime = !!this.ctx.getOutputTimestamp ? this.ctx.getOutputTimestamp().contextTime : undefined; + return (contextTime !== undefined ? contextTime : this.ctx.currentTime) * 1000; + } + } + nowSec() { + return this.now() / 1000; + } + serverNow() { + return this.serverTime(this.now()); + } + serverTime(localTimeMs) { + return localTimeMs + this.diff; + } + diffBuffer = new Array(); + diff = 0; + ctx; +} +class SampleFormat { + rate = 48000; + channels = 2; + bits = 16; + msRate() { + return this.rate / 1000; + } + toString() { + return this.rate + ":" + this.bits + ":" + this.channels; + } + sampleSize() { + if (this.bits == 24) { + return 4; + } + return this.bits / 8; + } + frameSize() { + return this.channels * this.sampleSize(); + } + durationMs(bytes) { + return (bytes / this.frameSize()) * this.msRate(); + } +} +class Decoder { + setHeader(_buffer) { + return new SampleFormat(); + } + decode(_chunk) { + return null; + } +} +class OpusDecoder extends Decoder { + setHeader(buffer) { + let view = new DataView(buffer); + let ID_OPUS = 0x4F505553; + if (buffer.byteLength < 12) { + console.error("Opus header too small: " + buffer.byteLength); + return null; + } + else if (view.getUint32(0, true) != ID_OPUS) { + console.error("Opus header too small: " + buffer.byteLength); + return null; + } + let format = new SampleFormat(); + format.rate = view.getUint32(4, true); + format.bits = view.getUint16(8, true); + format.channels = view.getUint16(10, true); + console.log("Opus samplerate: " + format.toString()); + return format; + } + decode(_chunk) { + return null; + } +} +class FlacDecoder extends Decoder { + constructor() { + super(); + this.decoder = Flac.create_libflac_decoder(true); + if (this.decoder) { + let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false); + console.log("Flac init: " + init_status); + Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true }); + } + this.sampleFormat = new SampleFormat(); + this.flacChunk = new ArrayBuffer(0); + // this.pcmChunk = new PcmChunkMessage(); + // Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals}); + // flac_ok &= init_status == 0; + // console.log("flac init : " + flac_ok);//DEBUG + } + decode(chunk) { + // console.log("Flac decode: " + chunk.payload.byteLength); + this.flacChunk = chunk.payload.slice(0); + this.pcmChunk = chunk; + this.pcmChunk.clearPayload(); + this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true }; + // console.log("Flac len: " + this.flacChunk.byteLength); + while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) { + Flac.FLAC__stream_decoder_get_state(this.decoder); + // let state = Flac.FLAC__stream_decoder_get_state(this.decoder); + // console.log("State: " + state); + } + // console.log("Pcm payload: " + this.pcmChunk!.payloadSize()); + if (this.cacheInfo.cachedBlocks > 0) { + let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate(); + // console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms"); + this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs); + } + return this.pcmChunk; + } + read_callback_fn(bufferSize) { + // console.log(' decode read callback, buffer bytes max=', bufferSize); + if (this.header) { + console.log(" header: " + this.header.byteLength); + let data = new Uint8Array(this.header); + this.header = null; + return { buffer: data, readDataLength: data.byteLength, error: false }; + } + else if (this.flacChunk) { + // console.log(" flacChunk: " + this.flacChunk.byteLength); + // a fresh read => next call to write will not be from cached data + this.cacheInfo.isCachedChunk = false; + let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength))); + this.flacChunk = this.flacChunk.slice(data.byteLength); + return { buffer: data, readDataLength: data.byteLength, error: false }; + } + return { buffer: new Uint8Array(0), readDataLength: 0, error: false }; + } + write_callback_fn(data, frameInfo) { + // console.log(" write frame metadata: " + frameInfo + ", len: " + data.length); + if (this.cacheInfo.isCachedChunk) { + // there was no call to read, so it's some cached data + this.cacheInfo.cachedBlocks += frameInfo.blocksize; + } + let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize); + let view = new DataView(payload); + for (let channel = 0; channel < frameInfo.channels; ++channel) { + let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength); + // console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize); + for (let i = 0; i < frameInfo.blocksize; ++i) { + view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true); + } + } + this.pcmChunk.addPayload(payload); + // console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize()); + } + /** @memberOf decode */ + metadata_callback_fn(data) { + console.info('meta data: ', data); + // let view = new DataView(data); + this.sampleFormat.rate = data.sampleRate; + this.sampleFormat.channels = data.channels; + this.sampleFormat.bits = data.bitsPerSample; + console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString()); + } + /** @memberOf decode */ + error_callback_fn(err, errMsg) { + console.error('decode error callback', err, errMsg); + } + setHeader(buffer) { + this.header = buffer.slice(0); + Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder); + return this.sampleFormat; + } + sampleFormat; + decoder; + header = null; + flacChunk; + pcmChunk; + cacheInfo = { isCachedChunk: false, cachedBlocks: 0 }; +} +class PlayBuffer { + constructor(buffer, playTime, source, destination) { + this.buffer = buffer; + this.playTime = playTime; + this.source = source; + this.source.buffer = this.buffer; + this.source.connect(destination); + this.onended = (_playBuffer) => { }; + } + onended; + start() { + this.source.onended = () => { + this.onended(this); + }; + this.source.start(this.playTime); + } + buffer; + playTime; + source; + num = 0; +} +class PcmDecoder extends Decoder { + setHeader(buffer) { + let sampleFormat = new SampleFormat(); + let view = new DataView(buffer); + sampleFormat.channels = view.getUint16(22, true); + sampleFormat.rate = view.getUint32(24, true); + sampleFormat.bits = view.getUint16(34, true); + return sampleFormat; + } + decode(chunk) { + return chunk; + } +} +class SnapStream { + constructor(baseUrl) { + this.baseUrl = baseUrl; + this.timeProvider = new TimeProvider(); + if (this.setupAudioContext()) { + this.connect(); + } + else { + alert("Sorry, but the Web Audio API is not supported by your browser"); + } + } + setupAudioContext() { + let AudioContext = window.AudioContext // Default + || window.webkitAudioContext // Safari and old versions of Chrome + || false; + if (AudioContext) { + let options; + options = { latencyHint: "playback", sampleRate: this.sampleFormat ? this.sampleFormat.rate : undefined }; + const chromeVersion = getChromeVersion(); + if ((chromeVersion !== null && chromeVersion < 55) || !window.AudioContext) { + // Some older browsers won't decode the stream if options are provided. + options = undefined; + } + this.ctx = new AudioContext(options); + this.gainNode = this.ctx.createGain(); + this.gainNode.connect(this.ctx.destination); + } + else { + // Web Audio API is not supported + return false; + } + return true; + } + static getClientId() { + return getPersistentValue("uniqueId", uuidv4()); + } + connect() { + this.streamsocket = new WebSocket(this.baseUrl + '/stream'); + this.streamsocket.binaryType = "arraybuffer"; + this.streamsocket.onmessage = (ev) => this.onMessage(ev); + this.streamsocket.onopen = () => { + console.log("on open"); + let hello = new HelloMessage(); + hello.mac = "00:00:00:00:00:00"; + hello.arch = "web"; + hello.os = navigator.platform; + hello.hostname = "Snapweb client"; + hello.uniqueId = SnapStream.getClientId(); + const versionElem = document.getElementsByTagName("meta").namedItem("version"); + hello.version = versionElem ? versionElem.content : "0.0.0"; + this.sendMessage(hello); + this.syncTime(); + this.syncHandle = window.setInterval(() => this.syncTime(), 1000); + }; + this.streamsocket.onerror = (ev) => { console.error('error:', ev); }; + this.streamsocket.onclose = () => { + window.clearInterval(this.syncHandle); + console.info('connection lost, reconnecting in 1s'); + setTimeout(() => this.connect(), 1000); + }; + } + onMessage(msg) { + let view = new DataView(msg.data); + let type = view.getUint16(0, true); + if (type == 1) { + let codec = new CodecMessage(msg.data); + console.log("Codec: " + codec.codec); + if (codec.codec == "flac") { + this.decoder = new FlacDecoder(); + } + else if (codec.codec == "pcm") { + this.decoder = new PcmDecoder(); + } + else if (codec.codec == "opus") { + this.decoder = new OpusDecoder(); + alert("Codec not supported: " + codec.codec); + } + else { + alert("Codec not supported: " + codec.codec); + } + if (this.decoder) { + this.sampleFormat = this.decoder.setHeader(codec.payload); + console.log("Sampleformat: " + this.sampleFormat.toString()); + if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) { + alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString()); + } + else { + if (this.bufferDurationMs != 0) { + this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate()); + } + if (window.AudioContext) { + // we are not using webkitAudioContext, so it's safe to setup a new AudioContext with the new samplerate + // since this code is not triggered by direct user input, we cannt create a webkitAudioContext here + this.stopAudio(); + this.setupAudioContext(); + } + this.ctx.resume(); + this.timeProvider.setAudioContext(this.ctx); + this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100; + // this.timeProvider = new TimeProvider(this.ctx); + this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs); + this.latency = (this.ctx.baseLatency !== undefined ? this.ctx.baseLatency : 0) + (this.ctx.outputLatency !== undefined ? this.ctx.outputLatency : 0); + console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency + ", latency: " + this.latency); + this.play(); + } + } + } + else if (type == 2) { + let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat); + if (this.decoder) { + let decoded = this.decoder.decode(pcmChunk); + if (decoded) { + this.stream.addChunk(decoded); + } + } + } + else if (type == 3) { + this.serverSettings = new ServerSettingsMessage(msg.data); + this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100; + this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency; + console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted); + } + else if (type == 4) { + if (this.timeProvider) { + let time = new TimeMessage(msg.data); + this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds()); + } + // console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff); + } + else { + console.info("Message not handled, type: " + type); + } + } + sendMessage(msg) { + msg.sent = new Tv(0, 0); + msg.sent.setMilliseconds(this.timeProvider.now()); + msg.id = ++this.msgId; + if (this.streamsocket.readyState == this.streamsocket.OPEN) { + this.streamsocket.send(msg.serialize()); + } + } + syncTime() { + let t = new TimeMessage(); + t.latency.setMilliseconds(this.timeProvider.now()); + this.sendMessage(t); + // console.log("prepareSource median: " + Math.round(this.median * 10) / 10); + } + stopAudio() { + // if (this.ctx) { + // this.ctx.close(); + // } + this.ctx.suspend(); + while (this.audioBuffers.length > 0) { + let buffer = this.audioBuffers.pop(); + buffer.onended = () => { }; + buffer.source.stop(); + } + while (this.freeBuffers.length > 0) { + this.freeBuffers.pop(); + } + } + stop() { + window.clearInterval(this.syncHandle); + this.stopAudio(); + if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) { + this.streamsocket.onclose = () => { }; + this.streamsocket.close(); + } + } + play() { + this.playTime = this.timeProvider.nowSec() + 0.1; + for (let i = 1; i <= this.audioBufferCount; ++i) { + this.playNext(); + } + } + playNext() { + let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate); + let playTimeMs = (this.playTime + this.latency) * 1000 - this.bufferMs; + this.stream.getNextBuffer(buffer, playTimeMs); + let source = this.ctx.createBufferSource(); + let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode); + this.audioBuffers.push(playBuffer); + playBuffer.num = ++this.bufferNum; + playBuffer.onended = (buffer) => { + // let diff = this.timeProvider.nowSec() - buffer.playTime; + this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer); + // console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length); + this.playNext(); + }; + playBuffer.start(); + this.playTime += this.bufferFrameCount / this.sampleFormat.rate; + } + baseUrl; + streamsocket; + playTime = 0; + msgId = 0; + bufferDurationMs = 80; // 0; + bufferFrameCount = 3844; // 9600; // 2400;//8192; + syncHandle = -1; + // ageBuffer: Array; + audioBuffers = new Array(); + freeBuffers = new Array(); + timeProvider; + stream; + ctx; // | undefined; + gainNode; + serverSettings; + decoder; + sampleFormat; + // median: number = 0; + audioBufferCount = 3; + bufferMs = 1000; + bufferNum = 0; + latency = 0; +} +//# sourceMappingURL=snapstream.js.map diff --git a/music_assistant/providers/snapcast/snapweb/speaker_icon.png b/music_assistant/providers/snapcast/snapweb/speaker_icon.png new file mode 100644 index 00000000..ae8554d7 Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/speaker_icon.png differ diff --git a/music_assistant/providers/snapcast/snapweb/stop.png b/music_assistant/providers/snapcast/snapweb/stop.png new file mode 100644 index 00000000..0d55b491 Binary files /dev/null and b/music_assistant/providers/snapcast/snapweb/stop.png differ diff --git a/music_assistant/providers/snapcast/snapweb/styles.css b/music_assistant/providers/snapcast/snapweb/styles.css new file mode 100644 index 00000000..06a8092a --- /dev/null +++ b/music_assistant/providers/snapcast/snapweb/styles.css @@ -0,0 +1,314 @@ +body { + background-color: rgb(246, 246, 246); + color: rgb(255, 255, 255); + font-family: 'Arial', sans-serif; + width: 100%; + margin: 0; + font-size: 20px; + overscroll-behavior: contain; +} + +/* width */ +::-webkit-scrollbar { + width: 10px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: #1f1f1f; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #333; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +input, textarea, button, select, a { + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +.navbar { + overflow: hidden; + background-color: #607d8b; + z-index: 1; /* Sit on top */ + padding: 13px; + color: white; + position: fixed; /* Set the navbar to fixed position */ + top: 0; /* Position the navbar at the top of the page */ + width: 100%; /* Full width */ + font-size: 21px; + font-weight: 500; + user-select: none; +} + +.play-button { + display: block; + position: absolute; + right: 34px; + top: 5px; + height: 40px; + width: 40px; +} + +.content { + margin-top: 62px +} + +.group { + float: none; + background-color: white; + box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.2); + clear: both; + padding: 8px; + margin: 10px 15px 10px 15px; + overflow: auto; + width: auto; + border-radius: 3px; + user-select: none; +} + +.group.muted { + opacity: 0.27; +} + +.groupheader { + /* margin: 10px; */ + width: auto; + height: fit-content; + /* padding: 10px; */ + padding-bottom: 0px; + display: grid; + grid-template-columns: min-content auto min-content; + grid-template-rows: min-content min-content; + grid-gap: 0px; +} + +.groupheader-separator { + height: 1px; + margin: 8px 0px; + border-width: 0px; + color: lightgray; + background-color: lightgray; +} + +.stream { + color: #686868; + grid-row: 1; + grid-column: 1/3; + width: fit-content; +} + +select { + background-color: transparent; + border: 0px; + width: 150px; + font-size: 20px; + color: #e3e3e3; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +.slidergroupdiv { + /* background: greenyellow; */ + display: flex; + justify-content: center; + align-items: center; + grid-row: 2; + grid-column: 2; +} + +.client { + /* text-align: left; */ + /* margin: 10px; */ + width: auto; + height: fit-content; + /* padding: 10px; */ + display: grid; + grid-template-columns: min-content auto min-content; + grid-template-rows: min-content min-content; + grid-gap: 0px; +/* align-items: center;*/ +} + +/* .client:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); +} */ + +.client.disconnected { + opacity: 0.27; +} + +.name { + color: #686868; + user-select: none; + /* background: red; */ + padding-top: 5px; + grid-row: 1; + grid-column: 1/3; + text-decoration: none; +} + +.editdiv { + background: violet; + grid-row: 0/4; + grid-column: 3; +} + +.edit-icon { + color: #686868; + text-decoration: none; +} + +.delete-icon { + color: #ff4081; + text-decoration: none; +} + +.edit-icons { + align-items: center; + display: flex; + grid-row: 1/3; + grid-column: 3; +} + +.edit-group-icon { + display: flex; + color: transparent; + align-items: center; + grid-row: 1/3; + grid-column: 3; + text-decoration: none; +} + +.mute-button { + color: #686868; + grid-row: 2; + grid-column: 1; +/* top: 50%;*/ + height: 25px; + width: 25px; + padding-left: 10px; + padding-right: 10px; + text-decoration: none; +} +/* +.cover-img { + color: #686868; + grid-row: 2; + grid-column: 1; + height: 50px; + width: 50px; + padding: 5px; + text-decoration: none; +} +*/ +.sliderdiv { + display: flex; + justify-content: center; + align-items: center; + grid-row: 2; + grid-column: 2; + /* padding-left: 40px; */ + /* display: inline-block; + text-align: left; + width: 250px; */ +} + +.slider { + writing-mode: bt-lr; + -webkit-appearance: none; + background: #dbdbdb; + outline: none; + -webkit-transition: .2s; + transition: opacity .2s; + height: 2px; + width: 90%; +} + +.slider::-moz-range-track { + padding: 6px; + background-color: transparent; + border: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + height: 12px; + width: 12px; + border-radius: 50%; + background: #ff4081; + cursor: pointer; +} + +.slider::-moz-range-thumb { + height: 12px; + width: 12px; + border-radius: 50%; + background: #ff4081; + cursor: pointer; +} + +.slider.muted { + opacity: 0.27; +} + + .client-settings { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +.client-setting-content { + background-color: #fefefe; + color: #686868; + margin: 15% auto; /* 15% from the top and centered */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ +} + +.client-input { + color: #686868; + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +input[type=submit] { + width: 100%; + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; +} + +input[type=submit]:hover { + background-color: #45a049; +} + +div.container { + border-radius: 5px; + background-color: #f2f2f2; + padding: 20px; +} diff --git a/music_assistant/providers/sonos/__init__.py b/music_assistant/providers/sonos/__init__.py new file mode 100644 index 00000000..beb9361f --- /dev/null +++ b/music_assistant/providers/sonos/__init__.py @@ -0,0 +1,52 @@ +""" +Sonos Player provider for Music Assistant for speakers running the S2 firmware. + +Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. +https://github.com/music-assistant/aiosonos +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from music_assistant.constants import VERBOSE_LOG_LEVEL + +from .provider import SonosPlayerProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + prov = SonosPlayerProvider(mass, manifest, config) + # set-up aiosonos logging + if prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("aiosonos").setLevel(logging.DEBUG) + else: + logging.getLogger("aiosonos").setLevel(prov.logger.level + 10) + return prov + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return () diff --git a/music_assistant/providers/sonos/const.py b/music_assistant/providers/sonos/const.py new file mode 100644 index 00000000..1daa076a --- /dev/null +++ b/music_assistant/providers/sonos/const.py @@ -0,0 +1,28 @@ +"""Constants for the Sonos (S2) provider.""" + +from __future__ import annotations + +from aiosonos.api.models import PlayBackState as SonosPlayBackState +from music_assistant_models.enums import PlayerFeature, PlayerState + +PLAYBACK_STATE_MAP = { + SonosPlayBackState.PLAYBACK_STATE_BUFFERING: PlayerState.PLAYING, + SonosPlayBackState.PLAYBACK_STATE_IDLE: PlayerState.IDLE, + SonosPlayBackState.PLAYBACK_STATE_PAUSED: PlayerState.PAUSED, + SonosPlayBackState.PLAYBACK_STATE_PLAYING: PlayerState.PLAYING, +} + +PLAYER_FEATURES_BASE = { + PlayerFeature.SYNC, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.PAUSE, + PlayerFeature.ENQUEUE, +} + +SOURCE_LINE_IN = "line_in" +SOURCE_AIRPLAY = "airplay" +SOURCE_SPOTIFY = "spotify" +SOURCE_UNKNOWN = "unknown" +SOURCE_RADIO = "radio" + +CONF_AIRPLAY_MODE = "airplay_mode" diff --git a/music_assistant/providers/sonos/helpers.py b/music_assistant/providers/sonos/helpers.py new file mode 100644 index 00000000..3a6c6f02 --- /dev/null +++ b/music_assistant/providers/sonos/helpers.py @@ -0,0 +1,23 @@ +"""Helpers for the Sonos (S2) Provider.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zeroconf import IPVersion + +if TYPE_CHECKING: + from zeroconf.asyncio import AsyncServiceInfo + + +def get_primary_ip_address(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + for address in discovery_info.parsed_addresses(IPVersion.V4Only): + if address.startswith("127"): + # filter out loopback address + continue + if address.startswith("169.254"): + # filter out APIPA address + continue + return address + return None diff --git a/music_assistant/providers/sonos/icon.svg b/music_assistant/providers/sonos/icon.svg new file mode 100644 index 00000000..60d9e677 --- /dev/null +++ b/music_assistant/providers/sonos/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/music_assistant/providers/sonos/manifest.json b/music_assistant/providers/sonos/manifest.json new file mode 100644 index 00000000..ba904cd3 --- /dev/null +++ b/music_assistant/providers/sonos/manifest.json @@ -0,0 +1,12 @@ +{ + "type": "player", + "domain": "sonos", + "name": "SONOS", + "description": "SONOS Player provider for Music Assistant.", + "codeowners": ["@music-assistant"], + "requirements": ["aiosonos==0.1.6"], + "documentation": "https://music-assistant.io/player-support/sonos/", + "multi_instance": false, + "builtin": false, + "mdns_discovery": ["_sonos._tcp.local."] +} diff --git a/music_assistant/providers/sonos/player.py b/music_assistant/providers/sonos/player.py new file mode 100644 index 00000000..9af19073 --- /dev/null +++ b/music_assistant/providers/sonos/player.py @@ -0,0 +1,471 @@ +""" +Sonos Player provider for Music Assistant for speakers running the S2 firmware. + +Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. +https://github.com/music-assistant/aiosonos + +SonosPlayer: Holds the details of the (discovered) Sonosplayer. +""" + +from __future__ import annotations + +import asyncio +import time +from collections.abc import Callable +from typing import TYPE_CHECKING + +import shortuuid +from aiohttp.client_exceptions import ClientConnectorError +from aiosonos.api.models import ContainerType, MusicService, SonosCapability +from aiosonos.api.models import PlayBackState as SonosPlayBackState +from aiosonos.client import SonosLocalApiClient +from aiosonos.const import EventType as SonosEventType +from aiosonos.const import SonosEvent +from aiosonos.exceptions import ConnectionFailed, FailedCommand +from music_assistant_models.enums import ( + EventType, + PlayerFeature, + PlayerState, + PlayerType, + RepeatMode, +) +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.constants import CONF_CROSSFADE + +from .const import ( + CONF_AIRPLAY_MODE, + PLAYBACK_STATE_MAP, + PLAYER_FEATURES_BASE, + SOURCE_AIRPLAY, + SOURCE_LINE_IN, + SOURCE_RADIO, + SOURCE_SPOTIFY, +) + +if TYPE_CHECKING: + from aiosonos.api.models import DiscoveryInfo as SonosDiscoveryInfo + from music_assistant_models.event import MassEvent + + from .provider import SonosPlayerProvider + + +class SonosPlayer: + """Holds the details of the (discovered) Sonosplayer.""" + + def __init__( + self, + prov: SonosPlayerProvider, + player_id: str, + discovery_info: SonosDiscoveryInfo, + ip_address: str, + ) -> None: + """Initialize the SonosPlayer.""" + self.prov = prov + self.mass = prov.mass + self.player_id = player_id + self.discovery_info = discovery_info + self.ip_address = ip_address + self.logger = prov.logger.getChild(player_id) + self.connected: bool = False + self.client = SonosLocalApiClient(self.ip_address, self.mass.http_session) + self.mass_player: Player | None = None + self._listen_task: asyncio.Task | None = None + # Sonos speakers can optionally have airplay (most S2 speakers do) + # and this airplay player can also be a player within MA. + # We can do some smart stuff if we link them together where possible. + # The player we can just guess from the sonos player id (mac address). + self.airplay_player_id = f"ap{self.player_id[7:-5].lower()}" + self.queue_version: str = shortuuid.random(8) + self._on_cleanup_callbacks: list[Callable[[], None]] = [] + + def get_linked_airplay_player( + self, enabled_only: bool = True, active_only: bool = False + ) -> Player | None: + """Return the linked airplay player if available/enabled.""" + if enabled_only and not self.mass.config.get_raw_player_config_value( + self.player_id, CONF_AIRPLAY_MODE + ): + return None + if not (airplay_player := self.mass.players.get(self.airplay_player_id)): + return None + if not airplay_player.available: + return None + if active_only and not airplay_player.powered: + return None + return airplay_player + + async def setup(self) -> None: + """Handle setup of the player.""" + # connect the player first so we can fail early + await self._connect(False) + + # collect supported features + supported_features = set(PLAYER_FEATURES_BASE) + if SonosCapability.AUDIO_CLIP in self.discovery_info["device"]["capabilities"]: + supported_features.add(PlayerFeature.PLAY_ANNOUNCEMENT) + if not self.client.player.has_fixed_volume: + supported_features.add(PlayerFeature.VOLUME_SET) + + # instantiate the MA player + self.mass_player = mass_player = Player( + player_id=self.player_id, + provider=self.prov.instance_id, + type=PlayerType.PLAYER, + name=self.discovery_info["device"]["name"] + or self.discovery_info["device"]["modelDisplayName"], + available=True, + # treat as powered at start if the player is playing/paused + powered=self.client.player.group.playback_state + in ( + SonosPlayBackState.PLAYBACK_STATE_PLAYING, + SonosPlayBackState.PLAYBACK_STATE_BUFFERING, + SonosPlayBackState.PLAYBACK_STATE_PAUSED, + ), + device_info=DeviceInfo( + model=self.discovery_info["device"]["modelDisplayName"], + manufacturer=self.prov.manifest.name, + address=self.ip_address, + ), + supported_features=tuple(supported_features), + ) + self.update_attributes() + await self.mass.players.register_or_update(mass_player) + + # register callback for state changed + self._on_cleanup_callbacks.append( + self.client.subscribe( + self._on_player_event, + ( + SonosEventType.GROUP_UPDATED, + SonosEventType.PLAYER_UPDATED, + ), + ) + ) + # register callback for airplay player state changes + self._on_cleanup_callbacks.append( + self.mass.subscribe( + self._on_airplay_player_event, + (EventType.PLAYER_UPDATED, EventType.PLAYER_ADDED), + self.airplay_player_id, + ) + ) + # register callback for playerqueue state changes + self._on_cleanup_callbacks.append( + self.mass.subscribe( + self._on_mass_queue_items_event, + EventType.QUEUE_ITEMS_UPDATED, + self.player_id, + ) + ) + + async def unload(self) -> None: + """Unload the player (disconnect + cleanup).""" + await self._disconnect() + self.mass.players.remove(self.player_id, False) + for callback in self._on_cleanup_callbacks: + callback() + + def reconnect(self, delay: float = 1) -> None: + """Reconnect the player.""" + # use a task_id to prevent multiple reconnects + task_id = f"sonos_reconnect_{self.player_id}" + self.mass.call_later(delay, self._connect, delay, task_id=task_id) + + async def cmd_stop(self) -> None: + """Send STOP command to given player.""" + if self.client.player.is_passive: + self.logger.debug("Ignore STOP command: Player is synced to another player.") + return + if ( + airplay := self.get_linked_airplay_player(True, True) + ) and airplay.state != PlayerState.IDLE: + # linked airplay player is active, redirect the command + self.logger.debug("Redirecting STOP command to linked airplay player.") + await self.mass.players.cmd_stop(airplay.player_id) + return + try: + await self.client.player.group.stop() + except FailedCommand as err: + if "ERROR_PLAYBACK_NO_CONTENT" not in str(err): + raise + + async def cmd_play(self) -> None: + """Send PLAY command to given player.""" + if self.client.player.is_passive: + self.logger.debug("Ignore STOP command: Player is synced to another player.") + return + if ( + airplay := self.get_linked_airplay_player(True, True) + ) and airplay.state != PlayerState.IDLE: + # linked airplay player is active, redirect the command + self.logger.debug("Redirecting PLAY command to linked airplay player.") + await self.mass.players.cmd_play(airplay.player_id) + return + await self.client.player.group.play() + + async def cmd_pause(self) -> None: + """Send PAUSE command to given player.""" + if self.client.player.is_passive: + self.logger.debug("Ignore STOP command: Player is synced to another player.") + return + if ( + airplay := self.get_linked_airplay_player(True, True) + ) and airplay.state != PlayerState.IDLE: + # linked airplay player is active, redirect the command + self.logger.debug("Redirecting PAUSE command to linked airplay player.") + await self.mass.players.cmd_pause(airplay.player_id) + return + await self.client.player.group.pause() + + async def cmd_volume_set(self, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + await self.client.player.set_volume(volume_level) + # sync volume level with airplay player + if airplay := self.get_linked_airplay_player(False): + if airplay.state not in (PlayerState.PLAYING, PlayerState.PAUSED): + airplay.volume_level = volume_level + + async def cmd_volume_mute(self, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + await self.client.player.set_volume(muted=muted) + + def update_attributes(self) -> None: # noqa: PLR0915 + """Update the player attributes.""" + if not self.mass_player: + return + self.mass_player.available = self.connected + if not self.connected: + return + if self.client.player.has_fixed_volume: + self.mass_player.volume_level = 100 + else: + self.mass_player.volume_level = self.client.player.volume_level or 0 + self.mass_player.volume_muted = self.client.player.volume_muted + + group_parent = None + if self.client.player.is_coordinator: + # player is group coordinator + active_group = self.client.player.group + self.mass_player.group_childs = ( + set(self.client.player.group_members) + if len(self.client.player.group_members) > 1 + else set() + ) + self.mass_player.synced_to = None + else: + # player is group child (synced to another player) + group_parent = self.prov.sonos_players.get(self.client.player.group.coordinator_id) + if not group_parent or not group_parent.client or not group_parent.client.player: + # handle race condition where the group parent is not yet discovered + return + active_group = group_parent.client.player.group + self.mass_player.group_childs = set() + self.mass_player.synced_to = active_group.coordinator_id + self.mass_player.active_source = active_group.coordinator_id + + if airplay := self.get_linked_airplay_player(True): + # linked airplay player is active, update media from there + self.mass_player.state = airplay.state + self.mass_player.powered = airplay.powered + self.mass_player.active_source = airplay.active_source + self.mass_player.elapsed_time = airplay.elapsed_time + self.mass_player.elapsed_time_last_updated = airplay.elapsed_time_last_updated + # mark 'next_previous' feature as unsupported when airplay mode is active + if PlayerFeature.NEXT_PREVIOUS in self.mass_player.supported_features: + self.mass_player.supported_features = ( + x + for x in self.mass_player.supported_features + if x != PlayerFeature.NEXT_PREVIOUS + ) + return + # ensure 'next_previous' feature is supported when airplay mode is not active + if PlayerFeature.NEXT_PREVIOUS not in self.mass_player.supported_features: + self.mass_player.supported_features = ( + *self.mass_player.supported_features, + PlayerFeature.NEXT_PREVIOUS, + ) + + # map playback state + self.mass_player.state = PLAYBACK_STATE_MAP[active_group.playback_state] + self.mass_player.elapsed_time = active_group.position + + # figure out the active source based on the container + container_type = active_group.container_type + active_service = active_group.active_service + container = active_group.playback_metadata.get("container") + if container_type == ContainerType.LINEIN: + self.mass_player.active_source = SOURCE_LINE_IN + elif container_type == ContainerType.AIRPLAY: + # check if the MA airplay player is active + airplay_player = self.mass.players.get(self.airplay_player_id) + if airplay_player and airplay_player.state in ( + PlayerState.PLAYING, + PlayerState.PAUSED, + ): + self.mass_player.active_source = airplay_player.active_source + else: + self.mass_player.active_source = SOURCE_AIRPLAY + elif container_type == ContainerType.STATION: + self.mass_player.active_source = SOURCE_RADIO + elif active_service == MusicService.SPOTIFY: + self.mass_player.active_source = SOURCE_SPOTIFY + elif active_service == MusicService.MUSIC_ASSISTANT: + if object_id := container.get("id", {}).get("objectId"): + self.mass_player.active_source = object_id.split(":")[-1] + else: + # its playing some service we did not yet map + self.mass_player.active_source = active_service + + # sonos has this weirdness that it maps idle to paused + # which is annoying to figure out if we want to resume or let + # MA back in control again. So for now, we just map it to idle here. + if ( + self.mass_player.state == PlayerState.PAUSED + and active_service != MusicService.MUSIC_ASSISTANT + ): + self.mass_player.state = PlayerState.IDLE + + # parse current media + self.mass_player.elapsed_time = self.client.player.group.position + self.mass_player.elapsed_time_last_updated = time.time() + current_media = None + if (current_item := active_group.playback_metadata.get("currentItem")) and ( + (track := current_item.get("track")) and track.get("name") + ): + track_images = track.get("images", []) + track_image_url = track_images[0].get("url") if track_images else None + track_duration_millis = track.get("durationMillis") + current_media = PlayerMedia( + uri=track.get("id", {}).get("objectId") or track.get("mediaUrl"), + title=track["name"], + artist=track.get("artist", {}).get("name"), + album=track.get("album", {}).get("name"), + duration=track_duration_millis / 1000 if track_duration_millis else None, + image_url=track_image_url, + ) + if active_service == MusicService.MUSIC_ASSISTANT: + current_media.queue_id = self.mass_player.active_source + current_media.queue_item_id = current_item["id"] + # radio stream info + if container and container.get("name") and active_group.playback_metadata.get("streamInfo"): + images = container.get("images", []) + image_url = images[0].get("url") if images else None + current_media = PlayerMedia( + uri=container.get("id", {}).get("objectId"), + title=active_group.playback_metadata["streamInfo"], + album=container["name"], + image_url=image_url, + ) + # generic info from container (also when MA is playing!) + if container and container.get("name") and container.get("id"): + if not current_media: + current_media = PlayerMedia(container["id"]["objectId"]) + if not current_media.image_url: + images = container.get("images", []) + current_media.image_url = images[0].get("url") if images else None + if not current_media.title: + current_media.title = container["name"] + if not current_media.uri: + current_media.uri = container["id"]["objectId"] + + self.mass_player.current_media = current_media + + async def _connect(self, retry_on_fail: int = 0) -> None: + """Connect to the Sonos player.""" + if self._listen_task and not self._listen_task.done(): + self.logger.debug("Already connected to Sonos player: %s", self.player_id) + return + try: + await self.client.connect() + except (ConnectionFailed, ClientConnectorError) as err: + self.logger.warning("Failed to connect to Sonos player: %s", err) + self.mass_player.available = False + self.mass.players.update(self.player_id) + if not retry_on_fail: + raise + self.reconnect(min(retry_on_fail + 30, 3600)) + return + self.connected = True + self.logger.debug("Connected to player API") + init_ready = asyncio.Event() + + async def _listener() -> None: + try: + await self.client.start_listening(init_ready) + except Exception as err: + if not isinstance(err, ConnectionFailed | asyncio.CancelledError): + self.logger.exception("Error in Sonos player listener: %s", err) + finally: + self.logger.info("Disconnected from player API") + if self.connected: + # we didn't explicitly disconnect, try to reconnect + # this should simply try to reconnect once and if that fails + # we rely on mdns to pick it up again later + await self._disconnect() + self.mass_player.available = False + self.mass.players.update(self.player_id) + self.reconnect(5) + + self._listen_task = asyncio.create_task(_listener()) + await init_ready.wait() + + async def _disconnect(self) -> None: + """Disconnect the client and cleanup.""" + self.connected = False + if self._listen_task and not self._listen_task.done(): + self._listen_task.cancel() + if self.client: + await self.client.disconnect() + self.logger.debug("Disconnected from player API") + + def _on_player_event(self, event: SonosEvent) -> None: + """Handle incoming event from player.""" + self.update_attributes() + self.mass.players.update(self.player_id) + + def _on_airplay_player_event(self, event: MassEvent) -> None: + """Handle incoming event from linked airplay player.""" + if not self.mass.config.get_raw_player_config_value(self.player_id, CONF_AIRPLAY_MODE): + return + if event.object_id != self.airplay_player_id: + return + self.update_attributes() + self.mass.players.update(self.player_id) + + async def _on_mass_queue_items_event(self, event: MassEvent) -> None: + """Handle incoming event from linked MA playerqueue.""" + # If the queue items changed and we have an active sonos queue, + # we need to inform the sonos queue to refresh the items. + if self.mass_player.active_source != event.object_id: + return + if not self.connected: + return + queue = self.mass.player_queues.get(event.object_id) + if not queue or queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): + return + if session_id := self.client.player.group.active_session_id: + await self.client.api.playback_session.refresh_cloud_queue(session_id) + + async def _on_mass_queue_event(self, event: MassEvent) -> None: + """Handle incoming event from linked MA playerqueue.""" + if self.mass_player.active_source != event.object_id: + return + if not self.connected: + return + # sync crossfade and repeat modes + queue = self.mass.player_queues.get(event.object_id) + if not queue or queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): + return + crossfade = await self.mass.config.get_player_config_value(queue.queue_id, CONF_CROSSFADE) + repeat_single_enabled = queue.repeat_mode == RepeatMode.ONE + repeat_all_enabled = queue.repeat_mode == RepeatMode.ALL + play_modes = self.client.player.group.play_modes + if ( + play_modes.crossfade != crossfade + or play_modes.repeat != repeat_all_enabled + or play_modes.repeat_one != repeat_single_enabled + ): + await self.client.player.group.set_play_modes( + crossfade=crossfade, repeat=repeat_all_enabled, repeat_one=repeat_single_enabled + ) diff --git a/music_assistant/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py new file mode 100644 index 00000000..9a611df0 --- /dev/null +++ b/music_assistant/providers/sonos/provider.py @@ -0,0 +1,513 @@ +""" +Sonos Player provider for Music Assistant for speakers running the S2 firmware. + +Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. +https://github.com/music-assistant/aiosonos +""" + +from __future__ import annotations + +import asyncio +import time +from typing import TYPE_CHECKING + +import shortuuid +from aiohttp import web +from aiohttp.client_exceptions import ClientError +from aiosonos.api.models import SonosCapability +from aiosonos.utils import get_discovery_info +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + ConfigEntry, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ConfigEntryType, ContentType, ProviderFeature +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, PlayerMedia +from zeroconf import ServiceStateChange + +from music_assistant.constants import MASS_LOGO_ONLINE, VERBOSE_LOG_LEVEL +from music_assistant.helpers import get_primary_ip_address +from music_assistant.helpers.tags import parse_tags +from music_assistant.models.player_provider import PlayerProvider + +from .const import CONF_AIRPLAY_MODE +from .player import SonosPlayer + +if TYPE_CHECKING: + from zeroconf.asyncio import AsyncServiceInfo + + +class SonosPlayerProvider(PlayerProvider): + """Sonos Player provider.""" + + sonos_players: dict[str, SonosPlayer] + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.sonos_players: dict[str, SonosPlayer] = {} + self.mass.streams.register_dynamic_route( + "/sonos_queue/v2.3/itemWindow", self._handle_sonos_queue_itemwindow + ) + self.mass.streams.register_dynamic_route( + "/sonos_queue/v2.3/version", self._handle_sonos_queue_version + ) + self.mass.streams.register_dynamic_route( + "/sonos_queue/v2.3/context", self._handle_sonos_queue_context + ) + self.mass.streams.register_dynamic_route( + "/sonos_queue/v2.3/timePlayed", self._handle_sonos_queue_time_played + ) + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + # disconnect all players + await asyncio.gather(*(player.unload() for player in self.sonos_players.values())) + self.sonos_players = None + self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/itemWindow") + self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/version") + self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/context") + self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/timePlayed") + + async def on_mdns_service_state_change( + self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None + ) -> None: + """Handle MDNS service state callback.""" + if state_change == ServiceStateChange.Removed: + # we don't listen for removed players here. + # instead we just wait for the player connection to fail + return + if "uuid" not in info.decoded_properties: + # not a S2 player + return + name = name.split("@", 1)[1] if "@" in name else name + player_id = info.decoded_properties["uuid"] + # handle update for existing device + if sonos_player := self.sonos_players.get(player_id): + if mass_player := sonos_player.mass_player: + cur_address = get_primary_ip_address(info) + if cur_address and cur_address != sonos_player.ip_address: + sonos_player.logger.debug( + "Address updated from %s to %s", sonos_player.ip_address, cur_address + ) + sonos_player.ip_address = cur_address + mass_player.device_info = DeviceInfo( + model=mass_player.device_info.model, + manufacturer=mass_player.device_info.manufacturer, + address=str(cur_address), + ) + if not sonos_player.connected: + self.logger.debug("Player back online: %s", mass_player.display_name) + sonos_player.client.player_ip = cur_address + # schedule reconnect + sonos_player.reconnect() + self.mass.players.update(player_id) + return + # handle new player setup in a delayed task because mdns announcements + # can arrive in (duplicated) bursts + task_id = f"setup_sonos_{player_id}" + self.mass.call_later(5, self._setup_player, player_id, name, info, task_id=task_id) + + async def get_player_config_entries( + self, + player_id: str, + ) -> tuple[ConfigEntry, ...]: + """Return Config Entries for the given player.""" + base_entries = ( + *await super().get_player_config_entries(player_id), + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + CONF_ENTRY_ENFORCE_MP3, + create_sample_rates_config_entry(48000, 24, 48000, 24, True), + ) + if not (sonos_player := self.sonos_players.get(player_id)): + # most probably the player is not yet discovered + return base_entries + return ( + *base_entries, + ConfigEntry( + key=CONF_AIRPLAY_MODE, + type=ConfigEntryType.BOOLEAN, + label="Enable Airplay mode (experimental)", + description="Almost all newer Sonos speakers have Airplay support. " + "If you have the Airplay provider enabled in Music Assistant, " + "your Sonos speakers will also be detected as Airplay speakers, meaning " + "you can group them with other Airplay speakers.\n\n" + "By default, Music Assistant uses the Sonos protocol for playback but with this " + "feature enabled, it will use the Airplay protocol instead by redirecting " + "the playback related commands to the linked Airplay player in Music Assistant, " + "allowing you to mix and match Sonos speakers with Airplay speakers. \n\n" + "TIP: When this feature is enabled, it make sense to set the underlying airplay " + "players to hide in the UI in the player settings to prevent duplicate players.", + required=False, + default_value=False, + hidden=SonosCapability.AIRPLAY + not in sonos_player.discovery_info["device"]["capabilities"], + ), + ) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.cmd_stop() + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.cmd_play() + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.cmd_pause() + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.cmd_volume_set(volume_level) + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.cmd_volume_mute(muted) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup master or group player. + """ + await self.cmd_sync_many(target_player, [player_id]) + + async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: + """Create temporary sync group by joining given players to target player.""" + sonos_player = self.sonos_players[target_player] + await sonos_player.client.player.group.modify_group_members( + player_ids_to_add=child_player_ids, player_ids_to_remove=[] + ) + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + sonos_player = self.sonos_players[player_id] + await sonos_player.client.player.leave_group() + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + sonos_player = self.sonos_players[player_id] + sonos_player.queue_version = shortuuid.random(8) + mass_player = self.mass.players.get(player_id) + if sonos_player.client.player.is_passive: + # this should be already handled by the player manager, but just in case... + msg = ( + f"Player {mass_player.display_name} can not " + "accept play_media command, it is synced to another player." + ) + raise PlayerCommandFailed(msg) + # for now always reset the active session + sonos_player.client.player.group.active_session_id = None + if airplay := sonos_player.get_linked_airplay_player(True): + # linked airplay player is active, redirect the command + self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.") + mass_player.active_source = airplay.active_source + # Sonos has an annoying bug (for years already, and they dont seem to care), + # where it looses its sync childs when airplay playback is (re)started. + # Try to handle it here with this workaround. + group_childs = ( + sonos_player.client.player.group_members + if len(sonos_player.client.player.group_members) > 1 + else [] + ) + if group_childs: + await self.mass.players.cmd_unsync_many(group_childs) + await self.mass.players.play_media(airplay.player_id, media) + if group_childs: + self.mass.call_later(5, self.cmd_sync_many, player_id, group_childs) + return + + if media.queue_id and media.queue_id.startswith("ugp_"): + # Special UGP stream - handle with play URL + await sonos_player.client.player.group.play_stream_url(media.uri, None) + return + + if media.queue_id: + # create a sonos cloud queue and load it + cloud_queue_url = f"{self.mass.streams.base_url}/sonos_queue/v2.3/" + await sonos_player.client.player.group.play_cloud_queue( + cloud_queue_url, + http_authorization=media.queue_id, + item_id=media.queue_item_id, + queue_version=sonos_player.queue_version, + ) + return + + # play a single uri/url + # note that this most probably will only work for (long running) radio streams + if self.mass.config.get_raw_player_config_value( + player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value + ): + media.uri = media.uri.replace(".flac", ".mp3") + await sonos_player.client.player.group.play_stream_url( + media.uri, {"name": media.title, "type": "track"} + ) + + async def cmd_next(self, player_id: str) -> None: + """Handle NEXT TRACK command for given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.client.player.group.skip_to_next_track() + + async def cmd_previous(self, player_id: str) -> None: + """Handle PREVIOUS TRACK command for given player.""" + if sonos_player := self.sonos_players[player_id]: + await sonos_player.client.player.group.skip_to_previous_track() + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next queue item on the player.""" + # We do nothing here as we handle the queue in the cloud queue endpoint. + # For sonos s2, instead of enqueuing tracks one by one, the sonos player itself + # can interact with our queue directly through the cloud queue endpoint. + + async def play_announcement( + self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None + ) -> None: + """Handle (provider native) playback of an announcement on given player.""" + sonos_player = self.sonos_players[player_id] + self.logger.debug( + "Playing announcement %s on %s", + announcement.uri, + sonos_player.mass_player.display_name, + ) + volume_level = self.mass.players.get_announcement_volume(player_id, volume_level) + await sonos_player.client.player.play_audio_clip( + announcement.uri, volume_level, name="Announcement" + ) + # Wait until the announcement is finished playing + # This is helpful for people who want to play announcements in a sequence + # yeah we can also setup a subscription on the sonos player for this, but this is easier + media_info = await parse_tags(announcement.uri) + duration = media_info.duration or 10 + await asyncio.sleep(duration) + + async def _setup_player(self, player_id: str, name: str, info: AsyncServiceInfo) -> None: + """Handle setup of a new player that is discovered using mdns.""" + assert player_id not in self.sonos_players + address = get_primary_ip_address(info) + if address is None: + return + if not self.mass.config.get_raw_player_config_value(player_id, "enabled", True): + self.logger.debug("Ignoring %s in discovery as it is disabled.", name) + return + try: + discovery_info = await get_discovery_info(self.mass.http_session, address) + except ClientError as err: + self.logger.debug("Ignoring %s in discovery as it is not reachable: %s", name, str(err)) + return + display_name = discovery_info["device"].get("name") or name + if SonosCapability.PLAYBACK not in discovery_info["device"]["capabilities"]: + # this will happen for satellite speakers in a surround/stereo setup + self.logger.debug( + "Ignoring %s in discovery as it is a passive satellite.", display_name + ) + return + self.logger.debug("Discovered Sonos device %s on %s", name, address) + self.sonos_players[player_id] = sonos_player = SonosPlayer( + self, player_id, discovery_info=discovery_info, ip_address=address + ) + await sonos_player.setup() + + async def _handle_sonos_queue_itemwindow(self, request: web.Request) -> web.Response: + """ + Handle the Sonos CloudQueue ItemWindow endpoint. + + https://docs.sonos.com/reference/itemwindow + """ + self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue ItemWindow request: %s", request.query) + sonos_playback_id = request.headers["X-Sonos-Playback-Id"] + sonos_player_id = sonos_playback_id.split(":")[0] + upcoming_window_size = int(request.query.get("upcomingWindowSize") or 10) + previous_window_size = int(request.query.get("previousWindowSize") or 10) + queue_version = request.query.get("queueVersion") + context_version = request.query.get("contextVersion") + if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)): + return web.Response(status=501) + if item_id := request.query.get("itemId"): + queue_index = self.mass.player_queues.index_by_id(mass_queue.queue_id, item_id) + else: + queue_index = mass_queue.current_index + if queue_index is None: + return web.Response(status=501) + offset = max(queue_index - previous_window_size, 0) + queue_items = self.mass.player_queues.items( + mass_queue.queue_id, + limit=upcoming_window_size + previous_window_size, + offset=max(queue_index - previous_window_size, 0), + ) + enforce_mp3 = self.mass.config.get_raw_player_config_value( + sonos_player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value + ) + sonos_queue_items = [ + { + "id": item.queue_item_id, + "deleted": not item.media_item.available, + "policies": {}, + "track": { + "type": "track", + "mediaUrl": self.mass.streams.resolve_stream_url( + item, output_codec=ContentType.MP3 if enforce_mp3 else ContentType.FLAC + ), + "contentType": "audio/flac", + "service": { + "name": "Music Assistant", + "id": "8", + "accountId": "", + "objectId": item.queue_item_id, + }, + "name": item.name, + "imageUrl": self.mass.metadata.get_image_url( + item.image, prefer_proxy=False, image_format="jpeg" + ) + if item.image + else None, + "durationMillis": item.duration * 1000 if item.duration else None, + "artist": { + "name": artist_str, + } + if item.media_item + and (artist_str := getattr(item.media_item, "artist_str", None)) + else None, + "album": { + "name": album.name, + } + if item.media_item and (album := getattr(item.media_item, "album", None)) + else None, + "quality": { + "bitDepth": item.streamdetails.audio_format.bit_depth, + "sampleRate": item.streamdetails.audio_format.sample_rate, + "codec": item.streamdetails.audio_format.content_type.value, + "lossless": item.streamdetails.audio_format.content_type.is_lossless(), + } + if item.streamdetails + else None, + }, + } + for item in queue_items + ] + result = { + "includesBeginningOfQueue": offset == 0, + "includesEndOfQueue": mass_queue.items <= (queue_index + len(sonos_queue_items)), + "contextVersion": context_version, + "queueVersion": queue_version, + "items": sonos_queue_items, + } + return web.json_response(result) + + async def _handle_sonos_queue_version(self, request: web.Request) -> web.Response: + """ + Handle the Sonos CloudQueue Version endpoint. + + https://docs.sonos.com/reference/version + """ + self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue Version request: %s", request.query) + sonos_playback_id = request.headers["X-Sonos-Playback-Id"] + sonos_player_id = sonos_playback_id.split(":")[0] + if not (sonos_player := self.sonos_players.get(sonos_player_id)): + return web.Response(status=501) + context_version = request.query.get("contextVersion") or "1" + queue_version = sonos_player.queue_version + result = {"contextVersion": context_version, "queueVersion": queue_version} + return web.json_response(result) + + async def _handle_sonos_queue_context(self, request: web.Request) -> web.Response: + """ + Handle the Sonos CloudQueue Context endpoint. + + https://docs.sonos.com/reference/context + """ + self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue Context request: %s", request.query) + sonos_playback_id = request.headers["X-Sonos-Playback-Id"] + sonos_player_id = sonos_playback_id.split(":")[0] + if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)): + return web.Response(status=501) + if not (sonos_player := self.sonos_players.get(sonos_player_id)): + return web.Response(status=501) + result = { + "contextVersion": "1", + "queueVersion": sonos_player.queue_version, + "container": { + "type": "playlist", + "name": "Music Assistant", + "imageUrl": MASS_LOGO_ONLINE, + "service": {"name": "Music Assistant", "id": "mass"}, + "id": { + "serviceId": "mass", + "objectId": f"mass:queue:{mass_queue.queue_id}", + "accountId": "", + }, + }, + "reports": { + "sendUpdateAfterMillis": 0, + "periodicIntervalMillis": 10000, + "sendPlaybackActions": True, + }, + "playbackPolicies": { + "canSkip": True, + "limitedSkips": False, + "canSkipToItem": True, + "canSkipBack": True, + "canSeek": False, # somehow not working correctly, investigate later + "canRepeat": True, + "canRepeatOne": True, + "canCrossfade": True, + "canShuffle": False, # handled by our queue controller itself + "showNNextTracks": 5, + "showNPreviousTracks": 5, + }, + } + return web.json_response(result) + + async def _handle_sonos_queue_time_played(self, request: web.Request) -> web.Response: + """ + Handle the Sonos CloudQueue TimePlayed endpoint. + + https://docs.sonos.com/reference/timeplayed + """ + self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue TimePlayed request: %s", request.query) + json_body = await request.json() + sonos_playback_id = request.headers["X-Sonos-Playback-Id"] + sonos_player_id = sonos_playback_id.split(":")[0] + if not (mass_player := self.mass.players.get(sonos_player_id)): + return web.Response(status=501) + if not (sonos_player := self.sonos_players.get(sonos_player_id)): + return web.Response(status=501) + for item in json_body["items"]: + if item["queueVersion"] != sonos_player.queue_version: + continue + if item["type"] != "update": + continue + if "positionMillis" not in item: + continue + mass_player.current_media = PlayerMedia( + uri=item["mediaUrl"], queue_id=sonos_playback_id, queue_item_id=item["id"] + ) + mass_player.elapsed_time = item["positionMillis"] / 1000 + mass_player.elapsed_time_last_updated = time.time() + self.mass.players.update(sonos_player_id) + break + return web.Response(status=204) diff --git a/music_assistant/providers/sonos_s1/__init__.py b/music_assistant/providers/sonos_s1/__init__.py new file mode 100644 index 00000000..d983d760 --- /dev/null +++ b/music_assistant/providers/sonos_s1/__init__.py @@ -0,0 +1,487 @@ +""" +Sonos Player S1 provider for Music Assistant. + +Based on the SoCo library for Sonos which uses the legacy/V1 UPnP API. + +Note that large parts of this code are copied over from the Home Assistant +integration for Sonos. +""" + +from __future__ import annotations + +import asyncio +import logging +from collections import OrderedDict +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ( + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + CONF_ENTRY_HTTP_PROFILE_FORCED_1, + ConfigEntry, + ConfigValueType, + create_sample_rates_config_entry, +) +from music_assistant_models.enums import ( + ConfigEntryType, + PlayerFeature, + PlayerState, + PlayerType, + ProviderFeature, +) +from music_assistant_models.errors import PlayerCommandFailed, PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from requests.exceptions import RequestException +from soco import config as soco_config +from soco import events_asyncio, zonegroupstate +from soco.discovery import discover, scan_network + +from music_assistant.constants import CONF_CROSSFADE, CONF_ENFORCE_MP3, VERBOSE_LOG_LEVEL +from music_assistant.helpers.didl_lite import create_didl_metadata +from music_assistant.models.player_provider import PlayerProvider + +from .player import SonosPlayer + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + from soco.core import SoCo + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +PLAYER_FEATURES = ( + PlayerFeature.SYNC, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.PAUSE, + PlayerFeature.ENQUEUE, +) + +CONF_NETWORK_SCAN = "network_scan" +CONF_HOUSEHOLD_ID = "household_id" +SUBSCRIPTION_TIMEOUT = 1200 +ZGS_SUBSCRIPTION_TIMEOUT = 2 + +CONF_ENTRY_SAMPLE_RATES = create_sample_rates_config_entry(48000, 16, 48000, 16, True) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + soco_config.EVENTS_MODULE = events_asyncio + zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT + prov = SonosPlayerProvider(mass, manifest, config) + # set-up soco logging + if prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): + logging.getLogger("soco").setLevel(logging.DEBUG) + else: + logging.getLogger("soco").setLevel(prov.logger.level + 10) + await prov.handle_async_init() + return prov + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + household_ids = await discover_household_ids(mass) + return ( + ConfigEntry( + key=CONF_NETWORK_SCAN, + type=ConfigEntryType.BOOLEAN, + label="Enable network scan for discovery", + default_value=False, + description="Enable network scan for discovery of players. \n" + "Can be used if (some of) your players are not automatically discovered.\n" + "Should normally not be needed", + ), + ConfigEntry( + key=CONF_HOUSEHOLD_ID, + type=ConfigEntryType.STRING, + label="Household ID", + default_value=household_ids[0] if household_ids else None, + description="Household ID for the Sonos (S1) system. Will be auto detected if empty.", + category="advanced", + required=False, + ), + ) + + +@dataclass +class UnjoinData: + """Class to track data necessary for unjoin coalescing.""" + + players: list[SonosPlayer] + event: asyncio.Event = field(default_factory=asyncio.Event) + + +class SonosPlayerProvider(PlayerProvider): + """Sonos Player provider.""" + + sonosplayers: dict[str, SonosPlayer] | None = None + _discovery_running: bool = False + _discovery_reschedule_timer: asyncio.TimerHandle | None = None + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.SYNC_PLAYERS,) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.sonosplayers: OrderedDict[str, SonosPlayer] = OrderedDict() + self.topology_condition = asyncio.Condition() + self.boot_counts: dict[str, int] = {} + self.mdns_names: dict[str, str] = {} + self.unjoin_data: dict[str, UnjoinData] = {} + self._discovery_running = False + self.hosts_in_error: dict[str, bool] = {} + self.discovery_lock = asyncio.Lock() + self.creation_lock = asyncio.Lock() + self._known_invisible: set[SoCo] = set() + + async def unload(self) -> None: + """Handle close/cleanup of the provider.""" + if self._discovery_reschedule_timer: + self._discovery_reschedule_timer.cancel() + self._discovery_reschedule_timer = None + # await any in-progress discovery + while self._discovery_running: + await asyncio.sleep(0.5) + await asyncio.gather(*(player.offline() for player in self.sonosplayers.values())) + if events_asyncio.event_listener: + await events_asyncio.event_listener.async_stop() + self.sonosplayers = None + + async def get_player_config_entries( + self, + player_id: str, + ) -> tuple[ConfigEntry, ...]: + """Return Config Entries for the given player.""" + base_entries = await super().get_player_config_entries(player_id) + if not (self.sonosplayers.get(player_id)): + # most probably a syncgroup + return ( + *base_entries, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + ) + return ( + *base_entries, + CONF_ENTRY_CROSSFADE, + CONF_ENTRY_SAMPLE_RATES, + CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + CONF_ENTRY_HTTP_PROFILE_FORCED_1, + ) + + def is_device_invisible(self, ip_address: str) -> bool: + """Check if device at provided IP is known to be invisible.""" + return any(x for x in self._known_invisible if x.ip_address == ip_address) + + async def cmd_stop(self, player_id: str) -> None: + """Send STOP command to given player.""" + sonos_player = self.sonosplayers[player_id] + if sonos_player.sync_coordinator: + self.logger.debug( + "Ignore STOP command for %s: Player is synced to another player.", + player_id, + ) + return + await asyncio.to_thread(sonos_player.soco.stop) + self.mass.call_later(2, sonos_player.poll_speaker) + + async def cmd_play(self, player_id: str) -> None: + """Send PLAY command to given player.""" + sonos_player = self.sonosplayers[player_id] + if sonos_player.sync_coordinator: + self.logger.debug( + "Ignore PLAY command for %s: Player is synced to another player.", + player_id, + ) + return + await asyncio.to_thread(sonos_player.soco.play) + sonos_player.mass_player.poll_interval = 5 + self.mass.call_later(2, sonos_player.poll_speaker) + + async def cmd_pause(self, player_id: str) -> None: + """Send PAUSE command to given player.""" + sonos_player = self.sonosplayers[player_id] + if sonos_player.sync_coordinator: + self.logger.debug( + "Ignore PLAY command for %s: Player is synced to another player.", + player_id, + ) + return + if "Pause" not in sonos_player.soco.available_actions: + # pause not possible + await self.cmd_stop(player_id) + return + await asyncio.to_thread(sonos_player.soco.pause) + self.mass.call_later(2, sonos_player.poll_speaker) + + async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: + """Send VOLUME_SET command to given player.""" + sonos_player = self.sonosplayers[player_id] + + def set_volume_level(player_id: str, volume_level: int) -> None: + sonos_player.soco.volume = volume_level + + await asyncio.to_thread(set_volume_level, player_id, volume_level) + self.mass.call_later(2, sonos_player.poll_speaker) + + async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: + """Send VOLUME MUTE command to given player.""" + + def set_volume_mute(player_id: str, muted: bool) -> None: + sonos_player = self.sonosplayers[player_id] + sonos_player.soco.mute = muted + + await asyncio.to_thread(set_volume_mute, player_id, muted) + + async def cmd_sync(self, player_id: str, target_player: str) -> None: + """Handle SYNC command for given player. + + Join/add the given player(id) to the given (master) player/sync group. + + - player_id: player_id of the player to handle the command. + - target_player: player_id of the syncgroup master or group player. + """ + sonos_player = self.sonosplayers[player_id] + sonos_master_player = self.sonosplayers[target_player] + await sonos_master_player.join([sonos_player]) + self.mass.call_later(2, sonos_player.poll_speaker) + + async def cmd_unsync(self, player_id: str) -> None: + """Handle UNSYNC command for given player. + + Remove the given player from any syncgroups it currently is synced to. + + - player_id: player_id of the player to handle the command. + """ + sonos_player = self.sonosplayers[player_id] + await sonos_player.unjoin() + self.mass.call_later(2, sonos_player.poll_speaker) + + async def play_media( + self, + player_id: str, + media: PlayerMedia, + ) -> None: + """Handle PLAY MEDIA on given player.""" + sonos_player = self.sonosplayers[player_id] + mass_player = self.mass.players.get(player_id) + if sonos_player.sync_coordinator: + # this should be already handled by the player manager, but just in case... + msg = ( + f"Player {mass_player.display_name} can not " + "accept play_media command, it is synced to another player." + ) + raise PlayerCommandFailed(msg) + if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3): + media.uri = media.uri.replace(".flac", ".mp3") + didl_metadata = create_didl_metadata(media) + await asyncio.to_thread(sonos_player.soco.play_uri, media.uri, meta=didl_metadata) + self.mass.call_later(2, sonos_player.poll_speaker) + sonos_player.mass_player.poll_interval = 5 + + async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: + """Handle enqueuing of the next queue item on the player.""" + sonos_player = self.sonosplayers[player_id] + if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3): + media.uri = media.uri.replace(".flac", ".mp3") + didl_metadata = create_didl_metadata(media) + # set crossfade according to player setting + crossfade = await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) + if sonos_player.crossfade != crossfade: + + def set_crossfade() -> None: + try: + sonos_player.soco.cross_fade = crossfade + sonos_player.crossfade = crossfade + except Exception as err: + self.logger.warning( + "Unable to set crossfade for player %s: %s", sonos_player.zone_name, err + ) + + await asyncio.to_thread(set_crossfade) + + try: + await asyncio.to_thread( + sonos_player.soco.avTransport.SetNextAVTransportURI, + [("InstanceID", 0), ("NextURI", media.uri), ("NextURIMetaData", didl_metadata)], + timeout=60, + ) + except Exception as err: + self.logger.warning( + "Unable to enqueue next track on player: %s: %s", sonos_player.zone_name, err + ) + else: + self.logger.debug( + "Enqued next track (%s) to player %s", + media.title or media.uri, + sonos_player.soco.player_name, + ) + + async def poll_player(self, player_id: str) -> None: + """Poll player for state updates.""" + if player_id not in self.sonosplayers: + return + sonos_player = self.sonosplayers[player_id] + # dynamically change the poll interval + if sonos_player.mass_player.state == PlayerState.PLAYING: + sonos_player.mass_player.poll_interval = 5 + elif sonos_player.mass_player.powered: + sonos_player.mass_player.poll_interval = 20 + else: + sonos_player.mass_player.poll_interval = 60 + try: + # the check_poll logic will work out what endpoints need polling now + # based on when we last received info from the device + if needs_poll := await sonos_player.check_poll(): + await sonos_player.poll_speaker() + # always update the attributes + sonos_player.update_player(signal_update=needs_poll) + except ConnectionResetError as err: + raise PlayerUnavailableError from err + + async def discover_players(self) -> None: + """Discover Sonos players on the network.""" + if self._discovery_running: + return + + allow_network_scan = self.config.get_value(CONF_NETWORK_SCAN) + if not (household_id := self.config.get_value(CONF_HOUSEHOLD_ID)): + household_id = "Sonos" + + def do_discover() -> None: + """Run discovery and add players in executor thread.""" + self._discovery_running = True + try: + self.logger.debug("Sonos discovery started...") + discovered_devices: set[SoCo] = discover( + timeout=30, household_id=household_id, allow_network_scan=allow_network_scan + ) + if discovered_devices is None: + discovered_devices = set() + # process new players + for soco in discovered_devices: + try: + self._add_player(soco) + except RequestException as err: + # player is offline + self.logger.debug("Failed to add SonosPlayer %s: %s", soco, err) + except Exception as err: + self.logger.warning( + "Failed to add SonosPlayer %s: %s", + soco, + err, + exc_info=err if self.logger.isEnabledFor(10) else None, + ) + finally: + self._discovery_running = False + + await self.mass.create_task(do_discover) + + def reschedule() -> None: + self._discovery_reschedule_timer = None + self.mass.create_task(self.discover_players()) + + # reschedule self once finished + self._discovery_reschedule_timer = self.mass.loop.call_later(1800, reschedule) + + def _add_player(self, soco: SoCo) -> None: + """Add discovered Sonos player.""" + player_id = soco.uid + # check if existing player changed IP + if existing := self.sonosplayers.get(player_id): + if existing.soco.ip_address != soco.ip_address: + existing.update_ip(soco.ip_address) + return + if not soco.is_visible: + return + enabled = self.mass.config.get_raw_player_config_value(player_id, "enabled", True) + if not enabled: + self.logger.debug("Ignoring disabled player: %s", player_id) + return + + speaker_info = soco.get_speaker_info(True, timeout=7) + if soco.uid not in self.boot_counts: + self.boot_counts[soco.uid] = soco.boot_seqnum + self.logger.debug("Adding new player: %s", speaker_info) + transport_info = soco.get_current_transport_info() + play_state = transport_info["current_transport_state"] + if not (mass_player := self.mass.players.get(soco.uid)): + mass_player = Player( + player_id=soco.uid, + provider=self.instance_id, + type=PlayerType.PLAYER, + name=soco.player_name, + available=True, + powered=play_state in ("PLAYING", "TRANSITIONING"), + supported_features=PLAYER_FEATURES, + device_info=DeviceInfo( + model=speaker_info["model_name"], + address=soco.ip_address, + manufacturer="SONOS", + ), + needs_poll=True, + poll_interval=30, + ) + self.sonosplayers[player_id] = sonos_player = SonosPlayer( + self, + soco=soco, + mass_player=mass_player, + ) + if not soco.fixed_volume: + mass_player.supported_features = ( + *mass_player.supported_features, + PlayerFeature.VOLUME_SET, + ) + asyncio.run_coroutine_threadsafe( + self.mass.players.register_or_update(sonos_player.mass_player), loop=self.mass.loop + ) + + +async def discover_household_ids(mass: MusicAssistant, prefer_s1: bool = True) -> list[str]: + """Discover the HouseHold ID of S1 speaker(s) the network.""" + if cache := await mass.cache.get("sonos_household_ids"): + return cache + household_ids: list[str] = [] + + def get_all_sonos_ips() -> set[SoCo]: + """Run full network discovery and return IP's of all devices found on the network.""" + discovered_zones: set[SoCo] | None + if discovered_zones := scan_network(multi_household=True): + return {zone.ip_address for zone in discovered_zones} + return set() + + all_sonos_ips = await asyncio.to_thread(get_all_sonos_ips) + for ip_address in all_sonos_ips: + async with mass.http_session.get(f"http://{ip_address}:1400/status/zp") as resp: + if resp.status == 200: + data = await resp.text() + if prefer_s1 and "2" in data: + continue + if "HouseholdControlID" in data: + household_id = data.split("")[1].split( + "" + )[0] + household_ids.append(household_id) + await mass.cache.set("sonos_household_ids", household_ids, 3600) + return household_ids diff --git a/music_assistant/providers/sonos_s1/helpers.py b/music_assistant/providers/sonos_s1/helpers.py new file mode 100644 index 00000000..b9476562 --- /dev/null +++ b/music_assistant/providers/sonos_s1/helpers.py @@ -0,0 +1,108 @@ +"""Helper methods for common tasks.""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload + +from music_assistant_models.errors import PlayerCommandFailed +from soco import SoCo +from soco.exceptions import SoCoException, SoCoUPnPException + +if TYPE_CHECKING: + from . import SonosPlayer + + +UID_PREFIX = "RINCON_" +UID_POSTFIX = "01400" + +_LOGGER = logging.getLogger(__name__) + +_T = TypeVar("_T", bound="SonosPlayer") +_R = TypeVar("_R") +_P = ParamSpec("_P") + +_FuncType = Callable[Concatenate[_T, _P], _R] +_ReturnFuncType = Callable[Concatenate[_T, _P], _R | None] + + +class SonosUpdateError(PlayerCommandFailed): + """Update failed.""" + + +@overload +def soco_error( + errorcodes: None = ..., +) -> Callable[[_FuncType[_T, _P, _R]], _FuncType[_T, _P, _R]]: ... + + +@overload +def soco_error( + errorcodes: list[str], +) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: ... + + +def soco_error( + errorcodes: list[str] | None = None, +) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: + """Filter out specified UPnP errors and raise exceptions for service calls.""" + + def decorator(funct: _FuncType[_T, _P, _R]) -> _ReturnFuncType[_T, _P, _R]: + """Decorate functions.""" + + def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: + """Wrap for all soco UPnP exception.""" + args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) + try: + result = funct(self, *args, **kwargs) + except (OSError, SoCoException, SoCoUPnPException, TimeoutError) as err: + error_code = getattr(err, "error_code", None) + function = funct.__qualname__ + if errorcodes and error_code in errorcodes: + _LOGGER.debug("Error code %s ignored in call to %s", error_code, function) + return None + + if (target := _find_target_identifier(self, args_soco)) is None: + msg = "Unexpected use of soco_error" + raise RuntimeError(msg) from err + + message = f"Error calling {function} on {target}: {err}" + raise SonosUpdateError(message) from err + + return result + + return wrapper + + return decorator + + +def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None: + """Extract the best available target identifier from the provided instance object.""" + if zone_name := getattr(instance, "zone_name", None): + # SonosPlayer instance + return zone_name + if soco := getattr(instance, "soco", fallback_soco): + # Holds a SoCo instance attribute + # Only use attributes with no I/O + return soco._player_name or soco.ip_address + return None + + +def hostname_to_uid(hostname: str) -> str: + """Convert a Sonos hostname to a uid.""" + if hostname.startswith("Sonos-"): + baseuid = hostname.removeprefix("Sonos-").replace(".local.", "") + elif hostname.startswith("sonos"): + baseuid = hostname.removeprefix("sonos").replace(".local.", "") + else: + msg = f"{hostname} is not a sonos device." + raise ValueError(msg) + return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" + + +def sync_get_visible_zones(soco: SoCo) -> set[SoCo]: + """Ensure I/O attributes are cached and return visible zones.""" + _ = soco.household_id + _ = soco.uid + return soco.visible_zones diff --git a/music_assistant/providers/sonos_s1/icon.png b/music_assistant/providers/sonos_s1/icon.png new file mode 100644 index 00000000..be274bf0 Binary files /dev/null and b/music_assistant/providers/sonos_s1/icon.png differ diff --git a/music_assistant/providers/sonos_s1/icon.svg b/music_assistant/providers/sonos_s1/icon.svg new file mode 100644 index 00000000..60d9e677 --- /dev/null +++ b/music_assistant/providers/sonos_s1/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/music_assistant/providers/sonos_s1/manifest.json b/music_assistant/providers/sonos_s1/manifest.json new file mode 100644 index 00000000..b33c6ace --- /dev/null +++ b/music_assistant/providers/sonos_s1/manifest.json @@ -0,0 +1,16 @@ +{ + "type": "player", + "domain": "sonos_s1", + "name": "SONOS S1", + "description": "SONOS Player provider for Music Assistant for the S1 hardware, based on the Soco library. Select this provider if you have Sonos devices on the S1 operating system (with the S1 Controller app)", + "codeowners": [ + "@music-assistant" + ], + "requirements": [ + "soco==0.30.5", + "defusedxml==0.7.1" + ], + "documentation": "https://music-assistant.io/player-support/sonos/", + "multi_instance": false, + "builtin": false +} diff --git a/music_assistant/providers/sonos_s1/player.py b/music_assistant/providers/sonos_s1/player.py new file mode 100644 index 00000000..29e42c3b --- /dev/null +++ b/music_assistant/providers/sonos_s1/player.py @@ -0,0 +1,812 @@ +""" +Sonos Player provider for Music Assistant: SonosPlayer object/model. + +Note that large parts of this code are copied over from the Home Assistant +integration for Sonos. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import datetime +import logging +import time +from collections.abc import Callable, Coroutine +from typing import TYPE_CHECKING, Any + +from music_assistant_models.enums import PlayerFeature, PlayerState +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, Player +from soco import SoCoException +from soco.core import ( + MUSIC_SRC_AIRPLAY, + MUSIC_SRC_LINE_IN, + MUSIC_SRC_RADIO, + MUSIC_SRC_SPOTIFY_CONNECT, + MUSIC_SRC_TV, + SoCo, +) +from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer + +from music_assistant.constants import VERBOSE_LOG_LEVEL +from music_assistant.helpers import SonosUpdateError, soco_error +from music_assistant.helpers.datetime import utc + +if TYPE_CHECKING: + from soco.events_base import Event as SonosEvent + from soco.events_base import SubscriptionBase + + from . import SonosPlayerProvider + +CALLBACK_TYPE = Callable[[], None] +LOGGER = logging.getLogger(__name__) + +PLAYER_FEATURES = ( + PlayerFeature.SYNC, + PlayerFeature.VOLUME_MUTE, + PlayerFeature.VOLUME_SET, +) +DURATION_SECONDS = "duration_in_s" +POSITION_SECONDS = "position_in_s" +SUBSCRIPTION_TIMEOUT = 1200 +ZGS_SUBSCRIPTION_TIMEOUT = 2 +AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) +AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 +SONOS_STATE_PLAYING = "PLAYING" +SONOS_STATE_TRANSITIONING = "TRANSITIONING" +NEVER_TIME = -1200.0 +RESUB_COOLDOWN_SECONDS = 10.0 +SUBSCRIPTION_SERVICES = { + # "alarmClock", + "avTransport", + # "contentDirectory", + "deviceProperties", + "renderingControl", + "zoneGroupTopology", +} +SUPPORTED_VANISH_REASONS = ("powered off", "sleeping", "switch to bluetooth", "upgrade") +UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] +LINEIN_SOURCES = (MUSIC_SRC_TV, MUSIC_SRC_LINE_IN) +SOURCE_AIRPLAY = "AirPlay" +SOURCE_LINEIN = "Line-in" +SOURCE_SPOTIFY_CONNECT = "Spotify Connect" +SOURCE_TV = "TV" +SOURCE_MAPPING = { + MUSIC_SRC_AIRPLAY: SOURCE_AIRPLAY, + MUSIC_SRC_TV: SOURCE_TV, + MUSIC_SRC_LINE_IN: SOURCE_LINEIN, + MUSIC_SRC_SPOTIFY_CONNECT: SOURCE_SPOTIFY_CONNECT, +} + + +class SonosSubscriptionsFailed(PlayerCommandFailed): + """Subscription creation failed.""" + + +class SonosPlayer: + """Wrapper around Sonos/SoCo with some additional attributes.""" + + def __init__( + self, + sonos_prov: SonosPlayerProvider, + soco: SoCo, + mass_player: Player, + ) -> None: + """Initialize SonosPlayer instance.""" + self.sonos_prov = sonos_prov + self.mass = sonos_prov.mass + self.player_id = soco.uid + self.soco = soco + self.logger = sonos_prov.logger + self.household_id: str = soco.household_id + self.subscriptions: list[SubscriptionBase] = [] + self.mass_player: Player = mass_player + self.available: bool = True + # cached attributes + self.crossfade: bool = False + self.play_mode: str | None = None + self.playback_status: str | None = None + self.channel: str | None = None + self.duration: float | None = None + self.image_url: str | None = None + self.source_name: str | None = None + self.title: str | None = None + self.uri: str | None = None + self.position: int | None = None + self.position_updated_at: datetime.datetime | None = None + self.loudness: bool = False + self.bass: int = 0 + self.treble: int = 0 + # Subscriptions and events + self._subscriptions: list[SubscriptionBase] = [] + self._subscription_lock: asyncio.Lock | None = None + self._last_activity: float = NEVER_TIME + self._resub_cooldown_expires_at: float | None = None + # Grouping + self.sync_coordinator: SonosPlayer | None = None + self.group_members: list[SonosPlayer] = [self] + self.group_members_ids: list[str] = [] + self._group_members_missing: set[str] = set() + + def __hash__(self) -> int: + """Return a hash of self.""" + return hash(self.player_id) + + @property + def zone_name(self) -> str: + """Return zone name.""" + if self.mass_player: + return self.mass_player.display_name + return self.soco.speaker_info["zone_name"] + + @property + def subscription_address(self) -> str: + """Return the current subscription callback address.""" + assert len(self._subscriptions) > 0 + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + + @property + def missing_subscriptions(self) -> set[str]: + """Return a list of missing service subscriptions.""" + subscribed_services = {sub.service.service_type for sub in self._subscriptions} + return SUBSCRIPTION_SERVICES - subscribed_services + + @property + def should_poll(self) -> bool: + """Return if this player should be polled/pinged.""" + if not self.available: + return True + return (time.monotonic() - self._last_activity) > self.mass_player.poll_interval + + def setup(self) -> None: + """Run initial setup of the speaker (NOT async friendly).""" + if self.soco.is_coordinator: + self.crossfade = self.soco.cross_fade + self.mass_player.volume_level = self.soco.volume + self.mass_player.volume_muted = self.soco.mute + self.loudness = self.soco.loudness + self.bass = self.soco.bass + self.treble = self.soco.treble + self.update_groups() + if not self.sync_coordinator: + self.poll_media() + + asyncio.run_coroutine_threadsafe(self.subscribe(), self.mass.loop) + + async def offline(self) -> None: + """Handle removal of speaker when unavailable.""" + if not self.available: + return + + if self._resub_cooldown_expires_at is None and not self.mass.closing: + self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS + self.logger.debug("Starting resubscription cooldown for %s", self.zone_name) + + self.available = False + self.mass_player.available = False + self.mass.players.update(self.player_id) + self._share_link_plugin = None + + await self.unsubscribe() + + def log_subscription_result(self, result: Any, event: str, level: int = logging.DEBUG) -> None: + """Log a message if a subscription action (create/renew/stop) results in an exception.""" + if not isinstance(result, Exception): + return + + if isinstance(result, asyncio.exceptions.TimeoutError): + message = "Request timed out" + exc_info = None + else: + message = str(result) + exc_info = result if not str(result) else None + + self.logger.log( + level, + "%s failed for %s: %s", + event, + self.zone_name, + message, + exc_info=exc_info if self.logger.isEnabledFor(10) else None, + ) + + async def subscribe(self) -> None: + """Initiate event subscriptions under an async lock.""" + if not self._subscription_lock: + self._subscription_lock = asyncio.Lock() + + async with self._subscription_lock: + try: + # Create event subscriptions. + subscriptions = [ + self._subscribe_target(getattr(self.soco, service), self._handle_event) + for service in self.missing_subscriptions + ] + if not subscriptions: + return + self.logger.log(VERBOSE_LOG_LEVEL, "Creating subscriptions for %s", self.zone_name) + results = await asyncio.gather(*subscriptions, return_exceptions=True) + for result in results: + self.log_subscription_result(result, "Creating subscription", logging.WARNING) + if any(isinstance(result, Exception) for result in results): + raise SonosSubscriptionsFailed + except SonosSubscriptionsFailed: + self.logger.warning("Creating subscriptions failed for %s", self.zone_name) + assert self._subscription_lock is not None + async with self._subscription_lock: + await self.offline() + + async def unsubscribe(self) -> None: + """Cancel all subscriptions.""" + if not self._subscriptions: + return + self.logger.log(VERBOSE_LOG_LEVEL, "Unsubscribing from events for %s", self.zone_name) + results = await asyncio.gather( + *(subscription.unsubscribe() for subscription in self._subscriptions), + return_exceptions=True, + ) + for result in results: + self.log_subscription_result(result, "Unsubscribe") + self._subscriptions = [] + + async def check_poll(self) -> bool: + """Validate availability of the speaker based on recent activity.""" + if not self.should_poll: + return False + self.logger.log(VERBOSE_LOG_LEVEL, "Polling player for availability...") + try: + await asyncio.to_thread(self.ping) + self._speaker_activity("ping") + except SonosUpdateError: + if not self.available: + return False # already offline + self.logger.warning( + "No recent activity and cannot reach %s, marking unavailable", + self.zone_name, + ) + await self.offline() + return True + + def update_ip(self, ip_address: str) -> None: + """Handle updated IP of a Sonos player (NOT async friendly).""" + if self.available: + return + self.logger.debug( + "Player IP-address changed from %s to %s", self.soco.ip_address, ip_address + ) + try: + self.ping() + except SonosUpdateError: + return + self.soco.ip_address = ip_address + self.setup() + self.mass_player.device_info = DeviceInfo( + model=self.mass_player.device_info.model, + address=ip_address, + manufacturer=self.mass_player.device_info.manufacturer, + ) + self.update_player() + + @soco_error() + def ping(self) -> None: + """Test device availability. Failure will raise SonosUpdateError.""" + self.soco.renderingControl.GetVolume([("InstanceID", 0), ("Channel", "Master")], timeout=1) + + async def join( + self, + members: list[SonosPlayer], + ) -> None: + """Sync given players/speakers with this player.""" + async with self.sonos_prov.topology_condition: + group: list[SonosPlayer] = await self.mass.create_task(self._join, members) + await self.wait_for_groups([group]) + + async def unjoin(self) -> None: + """Unjoin player from all/any groups.""" + async with self.sonos_prov.topology_condition: + await self.mass.create_task(self._unjoin) + await self.wait_for_groups([[self]]) + + def update_player(self, signal_update: bool = True) -> None: + """Update Sonos Player.""" + self._update_attributes() + if signal_update: + # send update to the player manager right away only if we are triggered from an event + # when we're just updating from a manual poll, the player manager + # will detect changes to the player object itself + self.mass.loop.call_soon_threadsafe(self.sonos_prov.mass.players.update, self.player_id) + + async def poll_speaker(self) -> None: + """Poll the speaker for updates.""" + + def _poll(): + """Poll the speaker for updates (NOT async friendly).""" + self.update_groups() + self.poll_media() + self.mass_player.volume_level = self.soco.volume + self.mass_player.volume_muted = self.soco.mute + + await asyncio.to_thread(_poll) + + @soco_error() + def poll_media(self) -> None: + """Poll information about currently playing media.""" + transport_info = self.soco.get_current_transport_info() + new_status = transport_info["current_transport_state"] + + if new_status == SONOS_STATE_TRANSITIONING: + return + + update_position = new_status != self.playback_status + self.playback_status = new_status + self.play_mode = self.soco.play_mode + self._set_basic_track_info(update_position=update_position) + self.update_player() + + async def _subscribe_target(self, target: SubscriptionBase, sub_callback: Callable) -> None: + """Create a Sonos subscription for given target.""" + subscription = await target.subscribe( + auto_renew=True, requested_timeout=SUBSCRIPTION_TIMEOUT + ) + + def on_renew_failed(exception: Exception) -> None: + """Handle a failed subscription renewal callback.""" + self.mass.create_task(self._renew_failed(exception)) + + subscription.callback = sub_callback + subscription.auto_renew_fail = on_renew_failed + self._subscriptions.append(subscription) + + async def _renew_failed(self, exception: Exception) -> None: + """Mark the speaker as offline after a subscription renewal failure. + + This is to reset the state to allow a future clean subscription attempt. + """ + if not self.available: + return + + self.log_subscription_result(exception, "Subscription renewal", logging.WARNING) + await self.offline() + + def _handle_event(self, event: SonosEvent) -> None: + """Handle SonosEvent callback.""" + service_type: str = event.service.service_type + self._speaker_activity(f"{service_type} subscription") + + if service_type == "DeviceProperties": + self.update_player() + return + if service_type == "AVTransport": + self._handle_avtransport_event(event) + return + if service_type == "RenderingControl": + self._handle_rendering_control_event(event) + return + if service_type == "ZoneGroupTopology": + self._handle_zone_group_topology_event(event) + return + + def _handle_avtransport_event(self, event: SonosEvent) -> None: + """Update information about currently playing media from an event.""" + # NOTE: The new coordinator can be provided in a media update event but + # before the ZoneGroupState updates. If this happens the playback + # state will be incorrect and should be ignored. Switching to the + # new coordinator will use its media. The regrouping process will + # be completed during the next ZoneGroupState update. + av_transport_uri = event.variables.get("av_transport_uri", "") + current_track_uri = event.variables.get("current_track_uri", "") + if av_transport_uri == current_track_uri and av_transport_uri.startswith("x-rincon:"): + new_coordinator_uid = av_transport_uri.split(":")[-1] + if new_coordinator_speaker := self.sonos_prov.sonosplayers.get(new_coordinator_uid): + self.logger.log( + 5, + "Media update coordinator (%s) received for %s", + new_coordinator_speaker.zone_name, + self.zone_name, + ) + self.sync_coordinator = new_coordinator_speaker + else: + self.logger.debug( + "Media update coordinator (%s) for %s not yet available", + new_coordinator_uid, + self.zone_name, + ) + return + + if crossfade := event.variables.get("current_crossfade_mode"): + self.crossfade = bool(int(crossfade)) + + # Missing transport_state indicates a transient error + if (new_status := event.variables.get("transport_state")) is None: + return + + # Ignore transitions, we should get the target state soon + if new_status == SONOS_STATE_TRANSITIONING: + return + + evars = event.variables + new_status = evars["transport_state"] + state_changed = new_status != self.playback_status + + self.play_mode = evars["current_play_mode"] + self.playback_status = new_status + + track_uri = evars["enqueued_transport_uri"] or evars["current_track_uri"] + audio_source = self.soco.music_source_from_uri(track_uri) + + self._set_basic_track_info(update_position=state_changed) + + if (ct_md := evars["current_track_meta_data"]) and not self.image_url: + if album_art_uri := getattr(ct_md, "album_art_uri", None): + # TODO: handle library mess here + self.image_url = album_art_uri + + et_uri_md = evars["enqueued_transport_uri_meta_data"] + if isinstance(et_uri_md, DidlPlaylistContainer): + self.playlist_name = et_uri_md.title + + if queue_size := evars.get("number_of_tracks", 0): + self.queue_size = int(queue_size) + + if audio_source == MUSIC_SRC_RADIO: + if et_uri_md: + self.channel = et_uri_md.title + + # Extra guards for S1 compatibility + if ct_md and hasattr(ct_md, "radio_show") and ct_md.radio_show: + radio_show = ct_md.radio_show.split(",")[0] + self.channel = " • ".join(filter(None, [self.channel, radio_show])) + + if isinstance(et_uri_md, DidlAudioBroadcast): + self.title = self.title or self.channel + + self.update_player() + + def _handle_rendering_control_event(self, event: SonosEvent) -> None: + """Update information about currently volume settings.""" + variables = event.variables + + if "volume" in variables: + volume = variables["volume"] + self.mass_player.volume_level = int(volume["Master"]) + + if mute := variables.get("mute"): + self.mass_player.volume_muted = mute["Master"] == "1" + + self.update_player() + + def _handle_zone_group_topology_event(self, event: SonosEvent) -> None: + """Handle callback for topology change event.""" + if "zone_player_uui_ds_in_group" not in event.variables: + return + asyncio.run_coroutine_threadsafe(self.create_update_groups_coro(event), self.mass.loop) + + async def _rebooted(self) -> None: + """Handle a detected speaker reboot.""" + self.logger.debug("%s rebooted, reconnecting", self.zone_name) + await self.offline() + self._speaker_activity("reboot") + + def update_groups(self) -> None: + """Update group topology when polling.""" + asyncio.run_coroutine_threadsafe(self.create_update_groups_coro(), self.mass.loop) + + def update_group_for_uid(self, uid: str) -> None: + """Update group topology if uid is missing.""" + if uid not in self._group_members_missing: + return + missing_zone = self.sonos_prov.sonosplayers[uid].zone_name + self.logger.debug("%s was missing, adding to %s group", missing_zone, self.zone_name) + self.update_groups() + + def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: + """Handle callback for topology change event.""" + + def _get_soco_group() -> list[str]: + """Ask SoCo cache for existing topology.""" + coordinator_uid = self.soco.uid + joined_uids = [] + with contextlib.suppress(OSError, SoCoException): + if self.soco.group and self.soco.group.coordinator: + coordinator_uid = self.soco.group.coordinator.uid + joined_uids = [ + p.uid + for p in self.soco.group.members + if p.uid != coordinator_uid and p.is_visible + ] + + return [coordinator_uid, *joined_uids] + + async def _extract_group(event: SonosEvent | None) -> list[str]: + """Extract group layout from a topology event.""" + group = event and event.zone_player_uui_ds_in_group + if group: + assert isinstance(group, str) + return group.split(",") + return await self.mass.create_task(_get_soco_group) + + def _regroup(group: list[str]) -> None: + """Rebuild internal group layout (async safe).""" + if group == [self.soco.uid] and self.group_members == [self] and self.group_members_ids: + # Skip updating existing single speakers in polling mode + return + + group_members = [] + group_members_ids = [] + + for uid in group: + speaker = self.sonos_prov.sonosplayers.get(uid) + if speaker: + self._group_members_missing.discard(uid) + group_members.append(speaker) + group_members_ids.append(uid) + else: + self._group_members_missing.add(uid) + self.logger.debug( + "%s group member unavailable (%s), will try again", + self.zone_name, + uid, + ) + return + + if self.group_members_ids == group_members_ids: + # Useful in polling mode for speakers with stereo pairs or surrounds + # as those "invisible" speakers will bypass the single speaker check + return + + self.sync_coordinator = None + self.group_members = group_members + self.group_members_ids = group_members_ids + self.mass.loop.call_soon_threadsafe(self.mass.players.update, self.player_id) + + for joined_uid in group[1:]: + joined_speaker: SonosPlayer = self.sonos_prov.sonosplayers.get(joined_uid) + if joined_speaker: + joined_speaker.sync_coordinator = self + joined_speaker.group_members = group_members + joined_speaker.group_members_ids = group_members_ids + joined_speaker.update_player() + + self.logger.debug("Regrouped %s: %s", self.zone_name, self.group_members_ids) + self.update_player() + + async def _handle_group_event(event: SonosEvent | None) -> None: + """Get async lock and handle event.""" + async with self.sonos_prov.topology_condition: + group = await _extract_group(event) + if self.soco.uid == group[0]: + _regroup(group) + self.sonos_prov.topology_condition.notify_all() + + return _handle_group_event(event) + + async def wait_for_groups(self, groups: list[list[SonosPlayer]]) -> None: + """Wait until all groups are present, or timeout.""" + + def _test_groups(groups: list[list[SonosPlayer]]) -> bool: + """Return whether all groups exist now.""" + for group in groups: + coordinator = group[0] + + # Test that coordinator is coordinating + current_group = coordinator.group_members + if coordinator != current_group[0]: + return False + + # Test that joined members match + if set(group[1:]) != set(current_group[1:]): + return False + + return True + + try: + async with asyncio.timeout(5): + while not _test_groups(groups): + await self.sonos_prov.topology_condition.wait() + except TimeoutError: + self.logger.warning("Timeout waiting for target groups %s", groups) + + any_speaker = next(iter(self.sonos_prov.sonosplayers.values())) + any_speaker.soco.zone_group_state.clear_cache() + + def _update_attributes(self) -> None: + """Update attributes of the MA Player from SoCo state.""" + # generic attributes (player_info) + self.mass_player.available = self.available + + if not self.available: + self.mass_player.powered = False + self.mass_player.state = PlayerState.IDLE + self.mass_player.synced_to = None + self.mass_player.group_childs = set() + return + + # transport info (playback state) + self.mass_player.state = current_state = _convert_state(self.playback_status) + + # power 'on' player if we detect its playing + if not self.mass_player.powered and ( + current_state == PlayerState.PLAYING + or ( + self.sync_coordinator + and self.sync_coordinator.mass_player.state == PlayerState.PLAYING + ) + ): + self.mass_player.powered = True + + # media info (track info) + self.mass_player.current_item_id = self.uri + if self.uri and self.mass.streams.base_url in self.uri and self.player_id in self.uri: + self.mass_player.active_source = self.player_id + else: + self.mass_player.active_source = self.source_name + if self.position is not None and self.position_updated_at is not None: + self.mass_player.elapsed_time = self.position + self.mass_player.elapsed_time_last_updated = self.position_updated_at.timestamp() + + # zone topology (syncing/grouping) details + if self.sync_coordinator: + # player is synced to another player + self.mass_player.synced_to = self.sync_coordinator.player_id + self.mass_player.group_childs = set() + self.mass_player.active_source = self.sync_coordinator.mass_player.active_source + elif len(self.group_members_ids) > 1: + # this player is the sync leader in a group + self.mass_player.synced_to = None + self.mass_player.group_childs = set(self.group_members_ids) + else: + # standalone player, not synced + self.mass_player.synced_to = None + self.mass_player.group_childs = set() + + def _set_basic_track_info(self, update_position: bool = False) -> None: + """Query the speaker to update media metadata and position info.""" + self.channel = None + self.duration = None + self.image_url = None + self.source_name = None + self.title = None + self.uri = None + + try: + track_info = self._poll_track_info() + except SonosUpdateError as err: + self.logger.warning("Fetching track info failed: %s", err) + return + if not track_info["uri"]: + return + self.uri = track_info["uri"] + + audio_source = self.soco.music_source_from_uri(self.uri) + if source := SOURCE_MAPPING.get(audio_source): + self.source_name = source + if audio_source in LINEIN_SOURCES: + self.position = None + self.position_updated_at = None + self.title = source + return + + self.artist = track_info.get("artist") + self.album_name = track_info.get("album") + self.title = track_info.get("title") + self.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position", -1)) + if playlist_position > 0: + self.queue_position = playlist_position + + self._update_media_position(track_info, force_update=update_position) + + def _update_media_position( + self, position_info: dict[str, int], force_update: bool = False + ) -> None: + """Update state when playing music tracks.""" + duration = position_info.get(DURATION_SECONDS) + current_position = position_info.get(POSITION_SECONDS) + + if not (duration or current_position): + self.position = None + self.position_updated_at = None + return + + should_update = force_update + self.duration = duration + + # player started reporting position? + if current_position is not None and self.position is None: + should_update = True + + # position jumped? + if current_position is not None and self.position is not None: + if self.playback_status == SONOS_STATE_PLAYING: + assert self.position_updated_at is not None + time_delta = utc() - self.position_updated_at + time_diff = time_delta.total_seconds() + else: + time_diff = 0 + + calculated_position = self.position + time_diff + + if abs(calculated_position - current_position) > 1.5: + should_update = True + + if current_position is None: + self.position = None + self.position_updated_at = None + elif should_update: + self.position = current_position + self.position_updated_at = utc() + + def _speaker_activity(self, source: str) -> None: + """Track the last activity on this speaker, set availability and resubscribe.""" + if self._resub_cooldown_expires_at: + if time.monotonic() < self._resub_cooldown_expires_at: + self.logger.debug( + "Activity on %s from %s while in cooldown, ignoring", + self.zone_name, + source, + ) + return + self._resub_cooldown_expires_at = None + + self.logger.log(VERBOSE_LOG_LEVEL, "Activity on %s from %s", self.zone_name, source) + self._last_activity = time.monotonic() + was_available = self.available + self.available = True + if not was_available: + self.update_player() + self.mass.loop.call_soon_threadsafe(self.mass.create_task, self.subscribe()) + + @soco_error() + def _join(self, members: list[SonosPlayer]) -> list[SonosPlayer]: + if self.sync_coordinator: + self.unjoin() + group = [self] + else: + group = self.group_members.copy() + + for player in members: + if player.soco.uid != self.soco.uid and player not in group: + player.soco.join(self.soco) + player.sync_coordinator = self + group.append(player) + + return group + + @soco_error() + def _unjoin(self) -> None: + if self.group_members == [self]: + return + self.soco.unjoin() + self.sync_coordinator = None + + @soco_error() + def _poll_track_info(self) -> dict[str, Any]: + """Poll the speaker for current track info. + + Add converted position values (NOT async fiendly). + """ + track_info: dict[str, Any] = self.soco.get_current_track_info() + track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) + track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) + return track_info + + +def _convert_state(sonos_state: str) -> PlayerState: + """Convert Sonos state to PlayerState.""" + if sonos_state == "PLAYING": + return PlayerState.PLAYING + if sonos_state == "TRANSITIONING": + return PlayerState.PLAYING + if sonos_state == "PAUSED_PLAYBACK": + return PlayerState.PAUSED + return PlayerState.IDLE + + +def _timespan_secs(timespan): + """Parse a time-span into number of seconds.""" + if timespan in ("", "NOT_IMPLEMENTED", None): + return None + return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) diff --git a/music_assistant/providers/soundcloud/__init__.py b/music_assistant/providers/soundcloud/__init__.py new file mode 100644 index 00000000..36013ca1 --- /dev/null +++ b/music_assistant/providers/soundcloud/__init__.py @@ -0,0 +1,443 @@ +"""Soundcloud support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import time +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import InvalidDataError, LoginFailed +from music_assistant_models.media_items import ( + Artist, + AudioFormat, + ContentType, + ImageType, + MediaItemImage, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails +from soundcloudpy import SoundcloudAsyncAPI + +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.music_provider import MusicProvider + +CONF_CLIENT_ID = "client_id" +CONF_AUTHORIZATION = "authorization" + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, +) + + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + if not config.get_value(CONF_CLIENT_ID) or not config.get_value(CONF_AUTHORIZATION): + msg = "Invalid login credentials" + raise LoginFailed(msg) + return SoundcloudMusicProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_CLIENT_ID, + type=ConfigEntryType.SECURE_STRING, + label="Client ID", + required=True, + ), + ConfigEntry( + key=CONF_AUTHORIZATION, + type=ConfigEntryType.SECURE_STRING, + label="Authorization", + required=True, + ), + ) + + +class SoundcloudMusicProvider(MusicProvider): + """Provider for Soundcloud.""" + + _headers = None + _context = None + _cookies = None + _signature_timestamp = 0 + _cipher = None + _user_id = None + _soundcloud = None + _me = None + + async def handle_async_init(self) -> None: + """Set up the Soundcloud provider.""" + client_id = self.config.get_value(CONF_CLIENT_ID) + auth_token = self.config.get_value(CONF_AUTHORIZATION) + self._soundcloud = SoundcloudAsyncAPI(auth_token, client_id, self.mass.http_session) + await self._soundcloud.login() + self._me = await self._soundcloud.get_account_details() + self._user_id = self._me["id"] + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + @classmethod + async def _run_async(cls, call: Callable, *args, **kwargs): # noqa: ANN206 + return await asyncio.to_thread(call, *args, **kwargs) + + async def search( + self, search_query: str, media_types=list[MediaType], limit: int = 10 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + result = SearchResults() + searchtypes = [] + if MediaType.ARTIST in media_types: + searchtypes.append("artist") + if MediaType.TRACK in media_types: + searchtypes.append("track") + if MediaType.PLAYLIST in media_types: + searchtypes.append("playlist") + + media_types = [ + x for x in media_types if x in (MediaType.ARTIST, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return result + + searchresult = await self._soundcloud.search(search_query, limit) + + for item in searchresult["collection"]: + media_type = item["kind"] + if media_type == "user" and MediaType.ARTIST in media_types: + result.artists.append(await self._parse_artist(item)) + elif media_type == "track" and MediaType.TRACK in media_types: + if item.get("duration") == item.get("full_duration"): + # skip if it's a preview track (e.g. in case of free accounts) + result.tracks.append(await self._parse_track(item)) + elif media_type == "playlist" and MediaType.PLAYLIST in media_types: + result.playlists.append(await self._parse_playlist(item)) + + return result + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Soundcloud.""" + time_start = time.time() + + following = await self._soundcloud.get_following(self._user_id) + self.logger.debug( + "Processing Soundcloud library artists took %s seconds", + round(time.time() - time_start, 2), + ) + for artist in following["collection"]: + try: + yield await self._parse_artist(artist) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse artist failed: %s", artist, exc_info=error) + continue + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from Soundcloud.""" + time_start = time.time() + async for item in self._soundcloud.get_account_playlists(): + try: + raw_playlist = item["playlist"] + except KeyError: + self.logger.debug( + "Unexpected Soundcloud API response when parsing playlists: %s", + item, + ) + continue + + try: + playlist = await self._soundcloud.get_playlist_details( + playlist_id=raw_playlist["id"], + ) + + yield await self._parse_playlist(playlist) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug( + "Failed to obtain Soundcloud playlist details: %s", + raw_playlist, + exc_info=error, + ) + continue + + self.logger.debug( + "Processing Soundcloud library playlists took %s seconds", + round(time.time() - time_start, 2), + ) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Soundcloud.""" + time_start = time.time() + async for item in self._soundcloud.get_tracks_liked(): + track = await self._soundcloud.get_track_details(item) + try: + yield await self._parse_track(track[0]) + except IndexError: + continue + except (KeyError, TypeError, InvalidDataError) as error: + self.logger.debug("Parse track failed: %s", track, exc_info=error) + continue + + self.logger.debug( + "Processing Soundcloud library tracks took %s seconds", + round(time.time() - time_start, 2), + ) + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + artist_obj = await self._soundcloud.get_user_details(user_id=prov_artist_id) + try: + artist = await self._parse_artist(artist_obj=artist_obj) if artist_obj else None + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse artist failed: %s", artist_obj, exc_info=error) + return artist + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + track_obj = await self._soundcloud.get_track_details(track_id=prov_track_id) + try: + track = await self._parse_track(track_obj[0]) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse track failed: %s", track_obj, exc_info=error) + return track + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + playlist_obj = await self._soundcloud.get_playlist_details(playlist_id=prov_playlist_id) + try: + playlist = await self._parse_playlist(playlist_obj) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse playlist failed: %s", playlist_obj, exc_info=error) + return playlist + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + if page > 0: + # TODO: soundcloud doesn't seem to support paging for playlist tracks ?! + return result + playlist_obj = await self._soundcloud.get_playlist_details(playlist_id=prov_playlist_id) + if "tracks" not in playlist_obj: + return result + for index, item in enumerate(playlist_obj["tracks"], 1): + # TODO: is it really needed to grab the entire track with an api call ? + song = await self._soundcloud.get_track_details(item["id"]) + try: + if track := await self._parse_track(song[0], index): + result.append(track) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse track failed: %s", song, exc_info=error) + continue + return result + + async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: + """Get a list of 25 most popular tracks for the given artist.""" + tracks_obj = await self._soundcloud.get_popular_tracks_user( + user_id=prov_artist_id, limit=25 + ) + tracks = [] + for item in tracks_obj["collection"]: + song = await self._soundcloud.get_track_details(item["id"]) + try: + track = await self._parse_track(song[0]) + tracks.append(track) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse track failed: %s", song, exc_info=error) + continue + return tracks + + async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + tracks_obj = await self._soundcloud.get_recommended(track_id=prov_track_id, limit=limit) + tracks = [] + for item in tracks_obj["collection"]: + song = await self._soundcloud.get_track_details(item["id"]) + try: + track = await self._parse_track(song[0]) + tracks.append(track) + except (KeyError, TypeError, InvalidDataError, IndexError) as error: + self.logger.debug("Parse track failed: %s", song, exc_info=error) + continue + + return tracks + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + url: str = await self._soundcloud.get_stream_url(track_id=item_id) + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + # let ffmpeg work out the details itself as + # soundcloud uses a mix of different content types and streaming methods + audio_format=AudioFormat( + content_type=ContentType.UNKNOWN, + ), + stream_type=StreamType.HLS + if url.startswith("https://cf-hls-media.sndcdn.com") + else StreamType.HTTP, + path=url, + ) + + async def _parse_artist(self, artist_obj: dict) -> Artist: + """Parse a Soundcloud user response to Artist model object.""" + artist_id = None + permalink = artist_obj["permalink"] + if artist_obj.get("id"): + artist_id = artist_obj["id"] + if not artist_id: + msg = "Artist does not have a valid ID" + raise InvalidDataError(msg) + artist_id = str(artist_id) + artist = Artist( + item_id=artist_id, + name=artist_obj["username"], + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f"https://soundcloud.com/{permalink}", + ) + }, + ) + if artist_obj.get("description"): + artist.metadata.description = artist_obj["description"] + if artist_obj.get("avatar_url"): + img_url = artist_obj["avatar_url"] + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img_url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + return artist + + async def _parse_playlist(self, playlist_obj: dict) -> Playlist: + """Parse a Soundcloud Playlist response to a Playlist object.""" + playlist_id = str(playlist_obj["id"]) + playlist = Playlist( + item_id=playlist_id, + provider=self.domain, + name=playlist_obj["title"], + provider_mappings={ + ProviderMapping( + item_id=playlist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + playlist.is_editable = False + if playlist_obj.get("description"): + playlist.metadata.description = playlist_obj["description"] + if playlist_obj.get("artwork_url"): + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=self._transform_artwork_url(playlist_obj["artwork_url"]), + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if playlist_obj.get("genre"): + playlist.metadata.genres = playlist_obj["genre"] + if playlist_obj.get("tag_list"): + playlist.metadata.style = playlist_obj["tag_list"] + return playlist + + async def _parse_track(self, track_obj: dict, playlist_position: int = 0) -> Track: + """Parse a Soundcloud Track response to a Track model object.""" + name, version = parse_title_and_version(track_obj["title"]) + track_id = str(track_obj["id"]) + track = Track( + item_id=track_id, + provider=self.domain, + name=name, + version=version, + duration=track_obj["duration"] / 1000, + provider_mappings={ + ProviderMapping( + item_id=track_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.MP3, + ), + url=track_obj["permalink_url"], + ) + }, + position=playlist_position, + ) + user_id = track_obj["user"]["id"] + user = await self._soundcloud.get_user_details(user_id) + artist = await self._parse_artist(user) + if artist and artist.item_id not in {x.item_id for x in track.artists}: + track.artists.append(artist) + + if track_obj.get("artwork_url"): + track.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=self._transform_artwork_url(track_obj["artwork_url"]), + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + if track_obj.get("description"): + track.metadata.description = track_obj["description"] + if track_obj.get("genre"): + track.metadata.genres = track_obj["genre"] + if track_obj.get("tag_list"): + track.metadata.style = track_obj["tag_list"] + return track + + def _transform_artwork_url(self, artwork_url: str) -> str: + """Patch artwork URL to a high quality thumbnail.""" + # This is undocumented in their API docs, but was previously + return artwork_url.replace("large", "t500x500") diff --git a/music_assistant/providers/soundcloud/icon.svg b/music_assistant/providers/soundcloud/icon.svg new file mode 100644 index 00000000..026d9566 --- /dev/null +++ b/music_assistant/providers/soundcloud/icon.svg @@ -0,0 +1 @@ + diff --git a/music_assistant/providers/soundcloud/manifest.json b/music_assistant/providers/soundcloud/manifest.json new file mode 100644 index 00000000..c7e67910 --- /dev/null +++ b/music_assistant/providers/soundcloud/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "soundcloud", + "name": "Soundcloud", + "description": "Support for the Soundcloud streaming provider in Music Assistant.", + "codeowners": ["@domanchi", "@gieljnssns"], + "requirements": ["soundcloudpy==0.1.0"], + "documentation": "https://music-assistant.io/music-providers/soundcloud/", + "multi_instance": true +} diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py new file mode 100644 index 00000000..3789a194 --- /dev/null +++ b/music_assistant/providers/spotify/__init__.py @@ -0,0 +1,1020 @@ +"""Spotify musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import contextlib +import os +import platform +import time +from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urlencode + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import ( + AudioError, + LoginFailed, + MediaNotFoundError, + ResourceTemporarilyUnavailable, + SetupFailedError, +) +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + AudioFormat, + ContentType, + ImageType, + MediaItemImage, + MediaItemType, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import VERBOSE_LOG_LEVEL +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.audio import get_chunksize +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import lock, parse_title_and_version +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +CONF_CLIENT_ID = "client_id" +CONF_ACTION_AUTH = "auth" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_ACTION_CLEAR_AUTH = "clear_auth" +SCOPE = [ + "playlist-read", + "playlist-read-private", + "playlist-read-collaborative", + "playlist-modify-public", + "playlist-modify-private", + "user-follow-modify", + "user-follow-read", + "user-library-read", + "user-library-modify", + "user-read-private", + "user-read-email", + "user-top-read", + "app-remote-control", + "streaming", + "user-read-playback-state", + "user-modify-playback-state", + "user-read-currently-playing", + "user-modify-private", + "user-modify", + "user-read-playback-position", + "user-read-recently-played", +] + +CALLBACK_REDIRECT_URL = "https://music-assistant.io/callback" + +CACHE_DIR = "/tmp/spotify_cache" # noqa: S108 +LIKED_SONGS_FAKE_PLAYLIST_ID_PREFIX = "liked_songs" +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + if config.get_value(CONF_REFRESH_TOKEN) in (None, ""): + msg = "Re-Authentication required" + raise SetupFailedError(msg) + return SpotifyProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + + if action == CONF_ACTION_AUTH: + # spotify PKCE auth flow + # https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow + import pkce + + code_verifier, code_challenge = pkce.generate_pkce_pair() + async with AuthenticationHelper(mass, cast(str, values["session_id"])) as auth_helper: + params = { + "response_type": "code", + "client_id": values.get(CONF_CLIENT_ID) or app_var(2), + "scope": " ".join(SCOPE), + "code_challenge_method": "S256", + "code_challenge": code_challenge, + "redirect_uri": CALLBACK_REDIRECT_URL, + "state": auth_helper.callback_url, + } + query_string = urlencode(params) + url = f"https://accounts.spotify.com/authorize?{query_string}" + result = await auth_helper.authenticate(url) + authorization_code = result["code"] + # now get the access token + params = { + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": CALLBACK_REDIRECT_URL, + "client_id": values.get(CONF_CLIENT_ID) or app_var(2), + "code_verifier": code_verifier, + } + async with mass.http_session.post( + "https://accounts.spotify.com/api/token", data=params + ) as response: + result = await response.json() + values[CONF_REFRESH_TOKEN] = result["refresh_token"] + + # handle action clear authentication + if action == CONF_ACTION_CLEAR_AUTH: + assert values + values[CONF_REFRESH_TOKEN] = None + + auth_required = values.get(CONF_REFRESH_TOKEN) in (None, "") + + if auth_required: + values[CONF_CLIENT_ID] = None + label_text = ( + "You need to authenticate to Spotify. Click the authenticate button below " + "to start the authentication process which will open in a new (popup) window, " + "so make sure to disable any popup blockers.\n\n" + "Also make sure to perform this action from your local network" + ) + elif action == CONF_ACTION_AUTH: + label_text = "Authenticated to Spotify. Press save to complete setup." + else: + label_text = "Authenticated to Spotify. No further action required." + + return ( + ConfigEntry( + key="label_text", + type=ConfigEntryType.LABEL, + label=label_text, + ), + ConfigEntry( + key=CONF_REFRESH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label=CONF_REFRESH_TOKEN, + hidden=True, + required=True, + value=values.get(CONF_REFRESH_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_CLIENT_ID, + type=ConfigEntryType.SECURE_STRING, + label="Client ID (optional)", + description="By default, a generic client ID is used which is heavy rate limited. " + "It is advised that you create your own Spotify Developer account and use " + "that client ID here to speedup performance. \n\n" + f"Use {CALLBACK_REDIRECT_URL} as callback URL.", + required=False, + value=values.get(CONF_CLIENT_ID) if values else None, + hidden=not auth_required, + ), + ConfigEntry( + key=CONF_ACTION_AUTH, + type=ConfigEntryType.ACTION, + label="Authenticate with Spotify", + description="This button will redirect you to Spotify to authenticate.", + action=CONF_ACTION_AUTH, + hidden=not auth_required, + ), + ConfigEntry( + key=CONF_ACTION_CLEAR_AUTH, + type=ConfigEntryType.ACTION, + label="Clear authentication", + description="Clear the current authentication details.", + action=CONF_ACTION_CLEAR_AUTH, + action_label="Clear authentication", + required=False, + hidden=auth_required, + ), + ) + + +class SpotifyProvider(MusicProvider): + """Implementation of a Spotify MusicProvider.""" + + _auth_info: str | None = None + _sp_user: dict[str, Any] | None = None + _librespot_bin: str | None = None + throttler: ThrottlerManager + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.config_dir = os.path.join(self.mass.storage_path, self.instance_id) + self.throttler = ThrottlerManager(rate_limit=1, period=2) + if self.config.get_value(CONF_CLIENT_ID): + # loosen the throttler a bit when a custom client id is used + self.throttler.rate_limit = 45 + self.throttler.period = 30 + # check if we have a librespot binary for this arch + await self.get_librespot_binary() + # try login which will raise if it fails + await self.login() + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ProviderFeature.PLAYLIST_CREATE, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, + ) + + @property + def name(self) -> str: + """Return (custom) friendly name for this provider instance.""" + if self._sp_user: + postfix = self._sp_user["display_name"] + return f"{self.manifest.name}: {postfix}" + return super().name + + async def search( + self, search_query: str, media_types=list[MediaType], limit: int = 5 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + searchresult = SearchResults() + searchtypes = [] + if MediaType.ARTIST in media_types: + searchtypes.append("artist") + if MediaType.ALBUM in media_types: + searchtypes.append("album") + if MediaType.TRACK in media_types: + searchtypes.append("track") + if MediaType.PLAYLIST in media_types: + searchtypes.append("playlist") + if not searchtypes: + return searchresult + searchtype = ",".join(searchtypes) + search_query = search_query.replace("'", "") + api_result = await self._get_data("search", q=search_query, type=searchtype, limit=limit) + if "artists" in api_result: + searchresult.artists += [ + self._parse_artist(item) + for item in api_result["artists"]["items"] + if (item and item["id"] and item["name"]) + ] + if "albums" in api_result: + searchresult.albums += [ + self._parse_album(item) + for item in api_result["albums"]["items"] + if (item and item["id"]) + ] + if "tracks" in api_result: + searchresult.tracks += [ + self._parse_track(item) + for item in api_result["tracks"]["items"] + if (item and item["id"]) + ] + if "playlists" in api_result: + searchresult.playlists += [ + self._parse_playlist(item) + for item in api_result["playlists"]["items"] + if (item and item["id"]) + ] + return searchresult + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve library artists from spotify.""" + endpoint = "me/following" + while True: + spotify_artists = await self._get_data( + endpoint, + type="artist", + limit=50, + ) + for item in spotify_artists["artists"]["items"]: + if item and item["id"]: + yield self._parse_artist(item) + if spotify_artists["artists"]["next"]: + endpoint = spotify_artists["artists"]["next"] + endpoint = endpoint.replace("https://api.spotify.com/v1/", "") + else: + break + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve library albums from the provider.""" + for item in await self._get_all_items("me/albums"): + if item["album"] and item["album"]["id"]: + yield self._parse_album(item["album"]) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + for item in await self._get_all_items("me/tracks"): + if item and item["track"]["id"]: + yield self._parse_track(item["track"]) + + def _get_liked_songs_playlist_id(self) -> str: + return f"{LIKED_SONGS_FAKE_PLAYLIST_ID_PREFIX}-{self.instance_id}" + + async def _get_liked_songs_playlist(self) -> Playlist: + liked_songs = Playlist( + item_id=self._get_liked_songs_playlist_id(), + provider=self.domain, + name=f'Liked Songs {self._sp_user["display_name"]}', # TODO to be translated + owner=self._sp_user["display_name"], + provider_mappings={ + ProviderMapping( + item_id=self._get_liked_songs_playlist_id(), + provider_domain=self.domain, + provider_instance=self.instance_id, + url="https://open.spotify.com/collection/tracks", + ) + }, + ) + + liked_songs.is_editable = False # TODO Editing requires special endpoints + + liked_songs.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path="https://misc.scdn.co/liked-songs/liked-songs-64.png", + provider=self.domain, + remotely_accessible=True, + ) + ] + + liked_songs.cache_checksum = str(time.time()) + + return liked_songs + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve playlists from the provider.""" + yield await self._get_liked_songs_playlist() + for item in await self._get_all_items("me/playlists"): + if item and item["id"]: + yield self._parse_playlist(item) + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + artist_obj = await self._get_data(f"artists/{prov_artist_id}") + return self._parse_artist(artist_obj) + + async def get_album(self, prov_album_id) -> Album: + """Get full album details by id.""" + album_obj = await self._get_data(f"albums/{prov_album_id}") + return self._parse_album(album_obj) + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + track_obj = await self._get_data(f"tracks/{prov_track_id}") + return self._parse_track(track_obj) + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + if prov_playlist_id == self._get_liked_songs_playlist_id(): + return await self._get_liked_songs_playlist() + + playlist_obj = await self._get_data(f"playlists/{prov_playlist_id}") + return self._parse_playlist(playlist_obj) + + async def get_album_tracks(self, prov_album_id) -> list[Track]: + """Get all album tracks for given album id.""" + return [ + self._parse_track(item) + for item in await self._get_all_items(f"albums/{prov_album_id}/tracks") + if item["id"] + ] + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + result: list[Track] = [] + uri = ( + "me/tracks" + if prov_playlist_id == self._get_liked_songs_playlist_id() + else f"playlists/{prov_playlist_id}/tracks" + ) + page_size = 50 + offset = page * page_size + spotify_result = await self._get_data(uri, limit=page_size, offset=offset) + for index, item in enumerate(spotify_result["items"], 1): + if not (item and item["track"] and item["track"]["id"]): + continue + # use count as position + track = self._parse_track(item["track"]) + track.position = offset + index + result.append(track) + return result + + async def get_artist_albums(self, prov_artist_id) -> list[Album]: + """Get a list of all albums for the given artist.""" + return [ + self._parse_album(item) + for item in await self._get_all_items( + f"artists/{prov_artist_id}/albums?include_groups=album,single,compilation" + ) + if (item and item["id"]) + ] + + async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: + """Get a list of 10 most popular tracks for the given artist.""" + artist = await self.get_artist(prov_artist_id) + endpoint = f"artists/{prov_artist_id}/top-tracks" + items = await self._get_data(endpoint) + return [ + self._parse_track(item, artist=artist) + for item in items["tracks"] + if (item and item["id"]) + ] + + async def library_add(self, item: MediaItemType): + """Add item to library.""" + if item.media_type == MediaType.ARTIST: + await self._put_data("me/following", {"ids": [item.item_id]}, type="artist") + elif item.media_type == MediaType.ALBUM: + await self._put_data("me/albums", {"ids": [item.item_id]}) + elif item.media_type == MediaType.TRACK: + await self._put_data("me/tracks", {"ids": [item.item_id]}) + elif item.media_type == MediaType.PLAYLIST: + await self._put_data(f"playlists/{item.item_id}/followers", data={"public": False}) + return True + + async def library_remove(self, prov_item_id, media_type: MediaType): + """Remove item from library.""" + if media_type == MediaType.ARTIST: + await self._delete_data("me/following", {"ids": [prov_item_id]}, type="artist") + elif media_type == MediaType.ALBUM: + await self._delete_data("me/albums", {"ids": [prov_item_id]}) + elif media_type == MediaType.TRACK: + await self._delete_data("me/tracks", {"ids": [prov_item_id]}) + elif media_type == MediaType.PLAYLIST: + await self._delete_data(f"playlists/{prov_item_id}/followers") + return True + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]): + """Add track(s) to playlist.""" + track_uris = [f"spotify:track:{track_id}" for track_id in prov_track_ids] + data = {"uris": track_uris} + await self._post_data(f"playlists/{prov_playlist_id}/tracks", data=data) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + track_uris = [] + for pos in positions_to_remove: + uri = f"playlists/{prov_playlist_id}/tracks" + spotify_result = await self._get_data(uri, limit=1, offset=pos - 1) + for item in spotify_result["items"]: + if not (item and item["track"] and item["track"]["id"]): + continue + track_uris.append({"uri": f'spotify:track:{item["track"]["id"]}'}) + data = {"tracks": track_uris} + await self._delete_data(f"playlists/{prov_playlist_id}/tracks", data=data) + + async def create_playlist(self, name: str) -> Playlist: + """Create a new playlist on provider with given name.""" + data = {"name": name, "public": False} + new_playlist = await self._post_data(f"users/{self._sp_user['id']}/playlists", data=data) + self._fix_create_playlist_api_bug(new_playlist) + return self._parse_playlist(new_playlist) + + async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + endpoint = "recommendations" + items = await self._get_data(endpoint, seed_tracks=prov_track_id, limit=limit) + return [self._parse_track(item) for item in items["tracks"] if (item and item["id"])] + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + return StreamDetails( + item_id=item_id, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.OGG, + ), + stream_type=StreamType.CUSTOM, + ) + + async def get_audio_stream( + self, streamdetails: StreamDetails, seek_position: int = 0 + ) -> AsyncGenerator[bytes, None]: + """Return the audio stream for the provider item.""" + librespot = await self.get_librespot_binary() + spotify_uri = f"spotify://track:{streamdetails.item_id}" + self.logger.log(VERBOSE_LOG_LEVEL, f"Start streaming {spotify_uri} using librespot") + args = [ + librespot, + "--cache", + CACHE_DIR, + "--system-cache", + self.config_dir, + "--cache-size-limit", + "1G", + "--passthrough", + "--bitrate", + "320", + "--backend", + "pipe", + "--single-track", + spotify_uri, + "--disable-discovery", + "--dither", + "none", + ] + if seek_position: + args += ["--start-position", str(int(seek_position))] + chunk_size = get_chunksize(streamdetails.audio_format) + stderr = None if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else False + bytes_received = 0 + async with AsyncProcess( + args, + stdout=True, + stderr=stderr, + name="librespot", + ) as librespot_proc: + async for chunk in librespot_proc.iter_any(chunk_size): + yield chunk + bytes_received += len(chunk) + + if librespot_proc.returncode != 0 or bytes_received == 0: + raise AudioError(f"Failed to stream track {spotify_uri}") + + def _parse_artist(self, artist_obj): + """Parse spotify artist object to generic layout.""" + artist = Artist( + item_id=artist_obj["id"], + provider=self.domain, + name=artist_obj["name"] or artist_obj["id"], + provider_mappings={ + ProviderMapping( + item_id=artist_obj["id"], + provider_domain=self.domain, + provider_instance=self.instance_id, + url=artist_obj["external_urls"]["spotify"], + ) + }, + ) + if "genres" in artist_obj: + artist.metadata.genres = set(artist_obj["genres"]) + if artist_obj.get("images"): + for img in artist_obj["images"]: + img_url = img["url"] + if "2a96cbd8b46e442fc41c2b86b821562f" not in img_url: + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img_url, + provider=self.instance_id, + remotely_accessible=True, + ) + ] + break + return artist + + def _parse_album(self, album_obj: dict): + """Parse spotify album object to generic layout.""" + name, version = parse_title_and_version(album_obj["name"]) + album = Album( + item_id=album_obj["id"], + provider=self.domain, + name=name, + version=version, + provider_mappings={ + ProviderMapping( + item_id=album_obj["id"], + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat(content_type=ContentType.OGG, bit_rate=320), + url=album_obj["external_urls"]["spotify"], + ) + }, + ) + if "external_ids" in album_obj and album_obj["external_ids"].get("upc"): + album.external_ids.add((ExternalID.BARCODE, "0" + album_obj["external_ids"]["upc"])) + if "external_ids" in album_obj and album_obj["external_ids"].get("ean"): + album.external_ids.add((ExternalID.BARCODE, album_obj["external_ids"]["ean"])) + + for artist_obj in album_obj["artists"]: + if not artist_obj.get("name") or not artist_obj.get("id"): + continue + album.artists.append(self._parse_artist(artist_obj)) + + with contextlib.suppress(ValueError): + album.album_type = AlbumType(album_obj["album_type"]) + + if "genres" in album_obj: + album.metadata.genre = set(album_obj["genres"]) + if album_obj.get("images"): + album.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=album_obj["images"][0]["url"], + provider=self.instance_id, + remotely_accessible=True, + ) + ] + if "label" in album_obj: + album.metadata.label = album_obj["label"] + if album_obj.get("release_date"): + album.year = int(album_obj["release_date"].split("-")[0]) + if album_obj.get("copyrights"): + album.metadata.copyright = album_obj["copyrights"][0]["text"] + if album_obj.get("explicit"): + album.metadata.explicit = album_obj["explicit"] + return album + + def _parse_track( + self, + track_obj: dict[str, Any], + artist=None, + ) -> Track: + """Parse spotify track object to generic layout.""" + name, version = parse_title_and_version(track_obj["name"]) + track = Track( + item_id=track_obj["id"], + provider=self.domain, + name=name, + version=version, + duration=track_obj["duration_ms"] / 1000, + provider_mappings={ + ProviderMapping( + item_id=track_obj["id"], + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.OGG, + bit_rate=320, + ), + url=track_obj["external_urls"]["spotify"], + available=not track_obj["is_local"] and track_obj["is_playable"], + ) + }, + disc_number=track_obj.get("disc_number", 0), + track_number=track_obj.get("track_number", 0), + ) + if isrc := track_obj.get("external_ids", {}).get("isrc"): + track.external_ids.add((ExternalID.ISRC, isrc)) + + if artist: + track.artists.append(artist) + for track_artist in track_obj.get("artists", []): + if not track_artist.get("name") or not track_artist.get("id"): + continue + artist = self._parse_artist(track_artist) + if artist and artist.item_id not in {x.item_id for x in track.artists}: + track.artists.append(artist) + + track.metadata.explicit = track_obj["explicit"] + if "preview_url" in track_obj: + track.metadata.preview = track_obj["preview_url"] + if "album" in track_obj: + track.album = self._parse_album(track_obj["album"]) + if track_obj["album"].get("images"): + track.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=track_obj["album"]["images"][0]["url"], + provider=self.instance_id, + remotely_accessible=True, + ) + ] + if track_obj.get("copyright"): + track.metadata.copyright = track_obj["copyright"] + if track_obj.get("explicit"): + track.metadata.explicit = True + if track_obj.get("popularity"): + track.metadata.popularity = track_obj["popularity"] + return track + + def _parse_playlist(self, playlist_obj): + """Parse spotify playlist object to generic layout.""" + playlist = Playlist( + item_id=playlist_obj["id"], + provider=self.domain, + name=playlist_obj["name"], + owner=playlist_obj["owner"]["display_name"], + provider_mappings={ + ProviderMapping( + item_id=playlist_obj["id"], + provider_domain=self.domain, + provider_instance=self.instance_id, + url=playlist_obj["external_urls"]["spotify"], + ) + }, + ) + playlist.is_editable = ( + playlist_obj["owner"]["id"] == self._sp_user["id"] or playlist_obj["collaborative"] + ) + if playlist_obj.get("images"): + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=playlist_obj["images"][0]["url"], + provider=self.instance_id, + remotely_accessible=True, + ) + ] + if playlist.owner is None: + playlist.owner = self._sp_user["display_name"] + playlist.cache_checksum = str(playlist_obj["snapshot_id"]) + return playlist + + @lock + async def login(self, force_refresh: bool = False) -> dict: + """Log-in Spotify and return Auth/token info.""" + # return existing token if we have one in memory + if ( + not force_refresh + and self._auth_info + and (self._auth_info["expires_at"] > (time.time() - 600)) + ): + return self._auth_info + # request new access token using the refresh token + if not (refresh_token := self.config.get_value(CONF_REFRESH_TOKEN)): + raise LoginFailed("Authentication required") + + client_id = self.config.get_value(CONF_CLIENT_ID) or app_var(2) + params = { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": client_id, + } + for _ in range(2): + async with self.mass.http_session.post( + "https://accounts.spotify.com/api/token", data=params + ) as response: + if response.status != 200: + err = await response.text() + if "revoked" in err: + # clear refresh token if it's invalid + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_REFRESH_TOKEN, "" + ) + raise LoginFailed(f"Failed to refresh access token: {err}") + # the token failed to refresh, we allow one retry + await asyncio.sleep(2) + continue + # if we reached this point, the token has been successfully refreshed + auth_info = await response.json() + auth_info["expires_at"] = int(auth_info["expires_in"] + time.time()) + self.logger.debug("Successfully refreshed access token") + break + else: + raise LoginFailed(f"Failed to refresh access token: {err}") + + # make sure that our updated creds get stored in memory + config + self._auth_info = auth_info + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_REFRESH_TOKEN, auth_info["refresh_token"], encrypted=True + ) + # check if librespot still has valid auth + librespot = await self.get_librespot_binary() + args = [ + librespot, + "--system-cache", + self.config_dir, + "--check-auth", + ] + ret_code, stdout = await check_output(*args) + if ret_code != 0: + # cached librespot creds are invalid, re-authenticate + # we can use the check-token option to send a new token to librespot + # librespot will then get its own token from spotify (somehow) and cache that. + args = [ + librespot, + "--system-cache", + self.config_dir, + "--check-auth", + "--access-token", + auth_info["access_token"], + ] + ret_code, stdout = await check_output(*args) + if ret_code != 0: + # this should not happen, but guard it just in case + err = stdout.decode("utf-8").strip() + raise LoginFailed(f"Failed to verify credentials on Librespot: {err}") + + # get logged-in user info + if not self._sp_user: + self._sp_user = userinfo = await self._get_data("me", auth_info=auth_info) + self.mass.metadata.set_default_preferred_language(userinfo["country"]) + self.logger.info("Successfully logged in to Spotify as %s", userinfo["display_name"]) + return auth_info + + async def _get_all_items(self, endpoint, key="items", **kwargs) -> list[dict]: + """Get all items from a paged list.""" + limit = 50 + offset = 0 + all_items = [] + while True: + kwargs["limit"] = limit + kwargs["offset"] = offset + result = await self._get_data(endpoint, **kwargs) + offset += limit + if not result or key not in result or not result[key]: + break + all_items += result[key] + if len(result[key]) < limit: + break + return all_items + + @throttle_with_retries + async def _get_data(self, endpoint, **kwargs) -> dict[str, Any]: + """Get data from api.""" + url = f"https://api.spotify.com/v1/{endpoint}" + kwargs["market"] = "from_token" + kwargs["country"] = "from_token" + if not (auth_info := kwargs.pop("auth_info", None)): + auth_info = await self.login() + headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" + async with ( + self.mass.http_session.get( + url, headers=headers, params=kwargs, ssl=True, timeout=120 + ) as response, + ): + # handle spotify rate limiter + if response.status == 429: + backoff_time = int(response.headers["Retry-After"]) + raise ResourceTemporarilyUnavailable( + "Spotify Rate Limiter", backoff_time=backoff_time + ) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + self._auth_info = None + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) + + # handle 404 not found, convert to MediaNotFoundError + if response.status == 404: + raise MediaNotFoundError(f"{endpoint} not found") + response.raise_for_status() + return await response.json(loads=json_loads) + + @throttle_with_retries + async def _delete_data(self, endpoint, data=None, **kwargs) -> None: + """Delete data from api.""" + url = f"https://api.spotify.com/v1/{endpoint}" + auth_info = kwargs.pop("auth_info", await self.login()) + headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} + async with self.mass.http_session.delete( + url, headers=headers, params=kwargs, json=data, ssl=False + ) as response: + # handle spotify rate limiter + if response.status == 429: + backoff_time = int(response.headers["Retry-After"]) + raise ResourceTemporarilyUnavailable( + "Spotify Rate Limiter", backoff_time=backoff_time + ) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + self._auth_info = None + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + response.raise_for_status() + + @throttle_with_retries + async def _put_data(self, endpoint, data=None, **kwargs) -> None: + """Put data on api.""" + url = f"https://api.spotify.com/v1/{endpoint}" + auth_info = kwargs.pop("auth_info", await self.login()) + headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} + async with self.mass.http_session.put( + url, headers=headers, params=kwargs, json=data, ssl=False + ) as response: + # handle spotify rate limiter + if response.status == 429: + backoff_time = int(response.headers["Retry-After"]) + raise ResourceTemporarilyUnavailable( + "Spotify Rate Limiter", backoff_time=backoff_time + ) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + self._auth_info = None + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) + + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + response.raise_for_status() + + @throttle_with_retries + async def _post_data(self, endpoint, data=None, **kwargs) -> dict[str, Any]: + """Post data on api.""" + url = f"https://api.spotify.com/v1/{endpoint}" + auth_info = kwargs.pop("auth_info", await self.login()) + headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} + async with self.mass.http_session.post( + url, headers=headers, params=kwargs, json=data, ssl=False + ) as response: + # handle spotify rate limiter + if response.status == 429: + backoff_time = int(response.headers["Retry-After"]) + raise ResourceTemporarilyUnavailable( + "Spotify Rate Limiter", backoff_time=backoff_time + ) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + self._auth_info = None + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) + # handle temporary server error + if response.status in (502, 503): + raise ResourceTemporarilyUnavailable(backoff_time=30) + response.raise_for_status() + return await response.json(loads=json_loads) + + async def get_librespot_binary(self): + """Find the correct librespot binary belonging to the platform.""" + # ruff: noqa: SIM102 + if self._librespot_bin is not None: + return self._librespot_bin + + async def check_librespot(librespot_path: str) -> str | None: + try: + returncode, output = await check_output(librespot_path, "--version") + if returncode == 0 and b"librespot" in output: + self._librespot_bin = librespot_path + return librespot_path + except OSError: + return None + + base_path = os.path.join(os.path.dirname(__file__), "bin") + system = platform.system().lower().replace("darwin", "macos") + architecture = platform.machine().lower() + + if bridge_binary := await check_librespot( + os.path.join(base_path, f"librespot-{system}-{architecture}") + ): + return bridge_binary + + msg = f"Unable to locate Librespot for {system}/{architecture}" + raise RuntimeError(msg) + + def _fix_create_playlist_api_bug(self, playlist_obj: dict[str, Any]) -> None: + """Fix spotify API bug where incorrect owner id is returned from Create Playlist.""" + if playlist_obj["owner"]["id"] != self._sp_user["id"]: + playlist_obj["owner"]["id"] = self._sp_user["id"] + playlist_obj["owner"]["display_name"] = self._sp_user["display_name"] + else: + self.logger.warning( + "FIXME: Spotify have fixed their Create Playlist API, this fix can be removed." + ) diff --git a/music_assistant/providers/spotify/bin/librespot-linux-aarch64 b/music_assistant/providers/spotify/bin/librespot-linux-aarch64 new file mode 100755 index 00000000..7b91c8ef Binary files /dev/null and b/music_assistant/providers/spotify/bin/librespot-linux-aarch64 differ diff --git a/music_assistant/providers/spotify/bin/librespot-linux-x86_64 b/music_assistant/providers/spotify/bin/librespot-linux-x86_64 new file mode 100755 index 00000000..1022c144 Binary files /dev/null and b/music_assistant/providers/spotify/bin/librespot-linux-x86_64 differ diff --git a/music_assistant/providers/spotify/bin/librespot-macos-arm64 b/music_assistant/providers/spotify/bin/librespot-macos-arm64 new file mode 100755 index 00000000..de3c183b Binary files /dev/null and b/music_assistant/providers/spotify/bin/librespot-macos-arm64 differ diff --git a/music_assistant/providers/spotify/icon.svg b/music_assistant/providers/spotify/icon.svg new file mode 100644 index 00000000..843cf994 --- /dev/null +++ b/music_assistant/providers/spotify/icon.svg @@ -0,0 +1 @@ + diff --git a/music_assistant/providers/spotify/manifest.json b/music_assistant/providers/spotify/manifest.json new file mode 100644 index 00000000..01574ac6 --- /dev/null +++ b/music_assistant/providers/spotify/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "spotify", + "name": "Spotify", + "description": "Support for the Spotify streaming provider in Music Assistant.", + "codeowners": ["@music-assistant"], + "requirements": ["pkce==1.0.3"], + "documentation": "https://music-assistant.io/music-providers/spotify/", + "multi_instance": true +} diff --git a/music_assistant/providers/test/__init__.py b/music_assistant/providers/test/__init__.py new file mode 100644 index 00000000..630f64bc --- /dev/null +++ b/music_assistant/providers/test/__init__.py @@ -0,0 +1,168 @@ +"""Test/Demo provider that creates a collection of fake media items.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING + +from music_assistant_models.enums import ( + ContentType, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + MediaItemImage, + MediaItemMetadata, + ProviderMapping, + Track, + UniqueList, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import MASS_LOGO, VARIOUS_ARTISTS_FANART +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +DEFAULT_THUMB = MediaItemImage( + type=ImageType.THUMB, + path=MASS_LOGO, + provider="builtin", + remotely_accessible=False, +) + +DEFAULT_FANART = MediaItemImage( + type=ImageType.FANART, + path=VARIOUS_ARTISTS_FANART, + provider="builtin", + remotely_accessible=False, +) + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return TestProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, # noqa: ARG001 + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, # noqa: ARG001 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + return () + + +class TestProvider(MusicProvider): + """Test/Demo provider that creates a collection of fake media items.""" + + @property + def is_streaming_provider(self) -> bool: + """Return True if the provider is a streaming provider.""" + return False + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return (ProviderFeature.LIBRARY_TRACKS,) + + async def get_track(self, prov_track_id: str) -> Track: + """Get full track details by id.""" + artist_idx, album_idx, track_idx = prov_track_id.split("_", 3) + return Track( + item_id=prov_track_id, + provider=self.instance_id, + name=f"Test Track {artist_idx} - {album_idx} - {track_idx}", + duration=5, + artists=UniqueList([await self.get_artist(artist_idx)]), + album=await self.get_album(f"{artist_idx}_{album_idx}"), + provider_mappings={ + ProviderMapping( + item_id=prov_track_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ), + }, + metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])), + disc_number=1, + track_number=int(track_idx), + ) + + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get full artist details by id.""" + return Artist( + item_id=prov_artist_id, + provider=self.instance_id, + name=f"Test Artist {prov_artist_id}", + metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB, DEFAULT_FANART])), + provider_mappings={ + ProviderMapping( + item_id=prov_artist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + ) + + async def get_album(self, prov_album_id: str) -> Album: + """Get full artist details by id.""" + artist_idx, album_idx = prov_album_id.split("_", 2) + return Album( + item_id=prov_album_id, + provider=self.instance_id, + name=f"Test Album {album_idx}", + artists=UniqueList([await self.get_artist(artist_idx)]), + provider_mappings={ + ProviderMapping( + item_id=prov_album_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + ) + }, + metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])), + ) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from the provider.""" + for artist_idx in range(50): + for album_idx in range(25): + for track_idx in range(25): + track_item_id = f"{artist_idx}_{album_idx}_{track_idx}" + yield await self.get_track(track_item_id) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a track/radio.""" + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=AudioFormat( + content_type=ContentType.MP3, + sample_rate=44100, + bit_depth=16, + channels=2, + ), + media_type=MediaType.TRACK, + stream_type=StreamType.HTTP, + path=item_id, + can_seek=True, + ) diff --git a/music_assistant/providers/test/icon.svg b/music_assistant/providers/test/icon.svg new file mode 100644 index 00000000..845920ca --- /dev/null +++ b/music_assistant/providers/test/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/music_assistant/providers/test/manifest.json b/music_assistant/providers/test/manifest.json new file mode 100644 index 00000000..a8cd64d7 --- /dev/null +++ b/music_assistant/providers/test/manifest.json @@ -0,0 +1,11 @@ +{ + "type": "music", + "domain": "test", + "name": "Test / demo provider", + "description": "Test/Demo provider that creates a collection of fake media items.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "", + "multi_instance": false, + "builtin": false +} diff --git a/music_assistant/providers/theaudiodb/__init__.py b/music_assistant/providers/theaudiodb/__init__.py new file mode 100644 index 00000000..ac37764a --- /dev/null +++ b/music_assistant/providers/theaudiodb/__init__.py @@ -0,0 +1,416 @@ +"""The AudioDB Metadata provider for Music Assistant.""" + +from __future__ import annotations + +from json import JSONDecodeError +from typing import TYPE_CHECKING, Any, cast + +import aiohttp.client_exceptions +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + ImageType, + LinkType, + MediaItemImage, + MediaItemLink, + MediaItemMetadata, + Track, + UniqueList, +) + +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.app_vars import app_var # type: ignore[attr-defined] +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.metadata_provider import MetadataProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +SUPPORTED_FEATURES = ( + ProviderFeature.ARTIST_METADATA, + ProviderFeature.ALBUM_METADATA, + ProviderFeature.TRACK_METADATA, +) + +IMG_MAPPING = { + "strArtistThumb": ImageType.THUMB, + "strArtistLogo": ImageType.LOGO, + "strArtistCutout": ImageType.CUTOUT, + "strArtistClearart": ImageType.CLEARART, + "strArtistWideThumb": ImageType.LANDSCAPE, + "strArtistFanart": ImageType.FANART, + "strArtistBanner": ImageType.BANNER, + "strAlbumThumb": ImageType.THUMB, + "strAlbumThumbHQ": ImageType.THUMB, + "strAlbumCDart": ImageType.DISCART, + "strAlbum3DCase": ImageType.OTHER, + "strAlbum3DFlat": ImageType.OTHER, + "strAlbum3DFace": ImageType.OTHER, + "strAlbum3DThumb": ImageType.OTHER, + "strTrackThumb": ImageType.THUMB, + "strTrack3DCase": ImageType.OTHER, +} + +LINK_MAPPING = { + "strWebsite": LinkType.WEBSITE, + "strFacebook": LinkType.FACEBOOK, + "strTwitter": LinkType.TWITTER, + "strLastFMChart": LinkType.LASTFM, +} + +ALBUMTYPE_MAPPING = { + "Single": AlbumType.SINGLE, + "Compilation": AlbumType.COMPILATION, + "Album": AlbumType.ALBUM, + "EP": AlbumType.EP, +} + +CONF_ENABLE_IMAGES = "enable_images" +CONF_ENABLE_ARTIST_METADATA = "enable_artist_metadata" +CONF_ENABLE_ALBUM_METADATA = "enable_album_metadata" +CONF_ENABLE_TRACK_METADATA = "enable_track_metadata" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return AudioDbMetadataProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_ENABLE_ARTIST_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist metadata.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_ALBUM_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of album metadata.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_TRACK_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of track metadata.", + default_value=False, + ), + ConfigEntry( + key=CONF_ENABLE_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist/album/track images", + default_value=True, + ), + ) + + +class AudioDbMetadataProvider(MetadataProvider): + """The AudioDB Metadata provider.""" + + throttler: Throttler + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self.cache = self.mass.cache + self.throttler = Throttler(rate_limit=1, period=1) + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: + """Retrieve metadata for artist on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_ARTIST_METADATA): + return None + if not artist.mbid: + # for 100% accuracy we require the musicbrainz id for all lookups + return None + if data := await self._get_data("artist-mb.php", i=artist.mbid): + if data.get("artists"): + return self.__parse_artist(data["artists"][0]) + return None + + async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: + """Retrieve metadata for album on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_ALBUM_METADATA): + return None + if mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP): + result = await self._get_data("album-mb.php", i=mbid) + if result and result.get("album"): + adb_album = result["album"][0] + return await self.__parse_album(album, adb_album) + # if there was no match on mbid, there will certainly be no match by name + return None + # fallback if no musicbrainzid: lookup by name + for album_artist in album.artists: + # make sure to include the version in the album name + album_name = f"{album.name} {album.version}" if album.version else album.name + result = await self._get_data("searchalbum.php?", s=album_artist.name, a=album_name) + if result and result.get("album"): + for item in result["album"]: + # some safety checks + if album_artist.mbid: + if album_artist.mbid != item["strMusicBrainzArtistID"]: + continue + elif not compare_strings(album_artist.name, item["strArtist"]): + continue + if compare_strings(album_name, item["strAlbum"], strict=False): + # match found ! + return await self.__parse_album(album, item) + return None + + async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None: + """Retrieve metadata for track on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_TRACK_METADATA): + return None + if track.mbid: + result = await self._get_data("track-mb.php", i=track.mbid) + if result and result.get("track"): + return await self.__parse_track(track, result["track"][0]) + # if there was no match on mbid, there will certainly be no match by name + return None + # fallback if no musicbrainzid: lookup by name + for track_artist in track.artists: + # make sure to include the version in the album name + track_name = f"{track.name} {track.version}" if track.version else track.name + result = await self._get_data("searchtrack.php?", s=track_artist.name, t=track_name) + if result and result.get("track"): + for item in result["track"]: + # some safety checks + if track_artist.mbid: + if track_artist.mbid != item["strMusicBrainzArtistID"]: + continue + elif not compare_strings(track_artist.name, item["strArtist"]): + continue + if ( # noqa: SIM114 + track.album + and (mb_rgid := track.album.get_external_id(ExternalID.MB_RELEASEGROUP)) + # AudioDb swapped MB Album ID and ReleaseGroup ID ?! + and mb_rgid != item["strMusicBrainzAlbumID"] + ): + continue + elif track.album and not compare_strings( + track.album.name, item["strAlbum"], strict=False + ): + continue + if not compare_strings(track_name, item["strTrack"], strict=False): + continue + return await self.__parse_track(track, item) + return None + + def __parse_artist(self, artist_obj: dict[str, Any]) -> MediaItemMetadata: + """Parse audiodb artist object to MediaItemMetadata.""" + metadata = MediaItemMetadata() + # generic data + metadata.label = artist_obj.get("strLabel") + metadata.style = artist_obj.get("strStyle") + if genre := artist_obj.get("strGenre"): + metadata.genres = {genre} + metadata.mood = artist_obj.get("strMood") + # links + metadata.links = set() + for key, link_type in LINK_MAPPING.items(): + if link := artist_obj.get(key): + metadata.links.add(MediaItemLink(type=link_type, url=link)) + # description/biography + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := artist_obj.get(f"strBiography{lang_country}") or ( + desc := artist_obj.get(f"strBiography{lang_code.upper()}") + ): + metadata.description = desc + else: + metadata.description = artist_obj.get("strBiographyEN") + # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata + metadata.images = UniqueList() + for key, img_type in IMG_MAPPING.items(): + for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): + if img := artist_obj.get(f"{key}{postfix}"): + metadata.images.append( + MediaItemImage( + type=img_type, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ) + else: + break + return metadata + + async def __parse_album(self, album: Album, adb_album: dict[str, Any]) -> MediaItemMetadata: + """Parse audiodb album object to MediaItemMetadata.""" + metadata = MediaItemMetadata() + # generic data + metadata.label = adb_album.get("strLabel") + metadata.style = adb_album.get("strStyle") + if genre := adb_album.get("strGenre"): + metadata.genres = {genre} + metadata.mood = adb_album.get("strMood") + # links + metadata.links = set() + if link := adb_album.get("strWikipediaID"): + metadata.links.add( + MediaItemLink(type=LinkType.WIKIPEDIA, url=f"https://wikipedia.org/wiki/{link}") + ) + if link := adb_album.get("strAllMusicID"): + metadata.links.add( + MediaItemLink(type=LinkType.ALLMUSIC, url=f"https://www.allmusic.com/album/{link}") + ) + + # description + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := adb_album.get(f"strDescription{lang_country}") or ( + desc := adb_album.get(f"strDescription{lang_code.upper()}") + ): + metadata.description = desc + else: + metadata.description = adb_album.get("strDescriptionEN") + metadata.review = adb_album.get("strReview") + # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata + metadata.images = UniqueList() + for key, img_type in IMG_MAPPING.items(): + for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): + if img := adb_album.get(f"{key}{postfix}"): + metadata.images.append( + MediaItemImage( + type=img_type, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ) + else: + break + # fill in some missing album info if needed + if not album.year: + album.year = int(adb_album.get("intYearReleased", "0")) + if album.album_type == AlbumType.UNKNOWN and adb_album.get("strReleaseFormat"): + releaseformat = cast(str, adb_album.get("strReleaseFormat")) + album.album_type = ALBUMTYPE_MAPPING.get(releaseformat, AlbumType.UNKNOWN) + # update the artist mbid while at it + for album_artist in album.artists: + if not compare_strings(album_artist.name, adb_album["strArtist"]): + continue + if not album_artist.mbid and album_artist.provider == "library": + album_artist.mbid = adb_album["strMusicBrainzArtistID"] + await self.mass.music.artists.update_item_in_library( + album_artist.item_id, + album_artist, + ) + return metadata + + async def __parse_track(self, track: Track, adb_track: dict[str, Any]) -> MediaItemMetadata: + """Parse audiodb track object to MediaItemMetadata.""" + metadata = MediaItemMetadata() + # generic data + metadata.lyrics = adb_track.get("strTrackLyrics") + metadata.style = adb_track.get("strStyle") + if genre := adb_track.get("strGenre"): + metadata.genres = {genre} + metadata.mood = adb_track.get("strMood") + # description + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := adb_track.get(f"strDescription{lang_country}") or ( + desc := adb_track.get(f"strDescription{lang_code.upper()}") + ): + metadata.description = desc + else: + metadata.description = adb_track.get("strDescriptionEN") + # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata + metadata.images = UniqueList([]) + for key, img_type in IMG_MAPPING.items(): + for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): + if img := adb_track.get(f"{key}{postfix}"): + metadata.images.append( + MediaItemImage( + type=img_type, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ) + else: + break + # update the artist mbid while at it + for album_artist in track.artists: + if not compare_strings(album_artist.name, adb_track["strArtist"]): + continue + if not album_artist.mbid and album_artist.provider == "library": + album_artist.mbid = adb_track["strMusicBrainzArtistID"] + await self.mass.music.artists.update_item_in_library( + album_artist.item_id, + album_artist, + ) + # update the album mbid while at it + if ( + track.album + and not track.album.get_external_id(ExternalID.MB_RELEASEGROUP) + and track.album.provider == "library" + and isinstance(track.album, Album) + ): + track.album.add_external_id( + ExternalID.MB_RELEASEGROUP, adb_track["strMusicBrainzAlbumID"] + ) + await self.mass.music.albums.update_item_in_library(track.album.item_id, track.album) + return metadata + + @use_cache(86400 * 30) + async def _get_data(self, endpoint: str, **kwargs: Any) -> dict[str, Any] | None: + """Get data from api.""" + url = f"https://theaudiodb.com/api/v1/json/{app_var(3)}/{endpoint}" + async with ( + self.throttler, + self.mass.http_session.get(url, params=kwargs, ssl=False) as response, + ): + try: + result = cast(dict[str, Any], await response.json()) + except ( + aiohttp.client_exceptions.ContentTypeError, + JSONDecodeError, + ): + self.logger.error("Failed to retrieve %s", endpoint) + text_result = await response.text() + self.logger.debug(text_result) + return None + except ( + aiohttp.client_exceptions.ClientConnectorError, + aiohttp.client_exceptions.ServerDisconnectedError, + TimeoutError, + ): + self.logger.warning("Failed to retrieve %s", endpoint) + return None + if "error" in result and "limit" in result["error"]: + self.logger.warning(result["error"]) + return None + return result diff --git a/music_assistant/providers/theaudiodb/manifest.json b/music_assistant/providers/theaudiodb/manifest.json new file mode 100644 index 00000000..9b2eaecf --- /dev/null +++ b/music_assistant/providers/theaudiodb/manifest.json @@ -0,0 +1,12 @@ +{ + "type": "metadata", + "domain": "theaudiodb", + "name": "The Audio DB", + "description": "TheAudioDB is a community Database of audio artwork and metadata with a JSON API.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "", + "multi_instance": false, + "builtin": true, + "icon": "folder-information" +} diff --git a/music_assistant/providers/tidal/__init__.py b/music_assistant/providers/tidal/__init__.py new file mode 100644 index 00000000..b42725cb --- /dev/null +++ b/music_assistant/providers/tidal/__init__.py @@ -0,0 +1,947 @@ +"""Tidal music provider support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import base64 +import pickle +from collections.abc import Callable +from contextlib import suppress +from datetime import datetime, timedelta +from enum import StrEnum +from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( + AlbumType, + CacheCategory, + ConfigEntryType, + ExternalID, + ImageType, + MediaType, + ProviderFeature, + StreamType, +) +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( + Album, + Artist, + AudioFormat, + ContentType, + ItemMapping, + MediaItemImage, + MediaItemType, + Playlist, + ProviderMapping, + SearchResults, + Track, + UniqueList, +) +from music_assistant_models.streamdetails import StreamDetails +from tidalapi import Album as TidalAlbum +from tidalapi import Artist as TidalArtist +from tidalapi import Config as TidalConfig +from tidalapi import Playlist as TidalPlaylist +from tidalapi import Session as TidalSession +from tidalapi import Track as TidalTrack +from tidalapi import exceptions as tidal_exceptions + +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.tags import AudioTags, parse_tags +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.models.music_provider import MusicProvider + +from .helpers import ( + DEFAULT_LIMIT, + add_playlist_tracks, + create_playlist, + get_album, + get_album_tracks, + get_artist, + get_artist_albums, + get_artist_toptracks, + get_library_albums, + get_library_artists, + get_library_playlists, + get_library_tracks, + get_playlist, + get_playlist_tracks, + get_similar_tracks, + get_stream, + get_track, + library_items_add_remove, + remove_playlist_tracks, + search, +) + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Awaitable + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + from tidalapi.media import Lyrics as TidalLyrics + from tidalapi.media import Stream as TidalStream + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + +TOKEN_TYPE = "Bearer" + +# Actions +CONF_ACTION_START_PKCE_LOGIN = "start_pkce_login" +CONF_ACTION_COMPLETE_PKCE_LOGIN = "auth" +CONF_ACTION_CLEAR_AUTH = "clear_auth" + +# Intermediate steps +CONF_TEMP_SESSION = "temp_session" +CONF_OOPS_URL = "oops_url" + +# Config keys +CONF_AUTH_TOKEN = "auth_token" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_USER_ID = "user_id" +CONF_EXPIRY_TIME = "expiry_time" +CONF_QUALITY = "quality" + +# Labels +LABEL_START_PKCE_LOGIN = "start_pkce_login_label" +LABEL_OOPS_URL = "oops_url_label" +LABEL_COMPLETE_PKCE_LOGIN = "complete_pkce_login_label" + +BROWSE_URL = "https://tidal.com/browse" +RESOURCES_URL = "https://resources.tidal.com/images" + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +class TidalQualityEnum(StrEnum): + """Enum for Tidal Quality.""" + + HIGH_LOSSLESS = "LOSSLESS" # "High - 16bit, 44.1kHz" + HI_RES = "HI_RES_LOSSLESS" # "Max - Up to 24bit, 192kHz" + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return TidalProvider(mass, manifest, config) + + +async def tidal_auth_url(auth_helper: AuthenticationHelper, quality: str) -> str: + """Generate the Tidal authentication URL.""" + + def inner() -> TidalSession: + # global glob_temp_session + config = TidalConfig(quality=quality, item_limit=10000, alac=False) + session = TidalSession(config=config) + url = session.pkce_login_url() + auth_helper.send_url(url) + session_bytes = pickle.dumps(session) + base64_bytes = base64.b64encode(session_bytes) + return base64_bytes.decode("utf-8") + + return await asyncio.to_thread(inner) + + +async def tidal_pkce_login(base64_session: str, url: str) -> TidalSession: + """Async wrapper around the tidalapi Session function.""" + + def inner() -> TidalSession: + base64_bytes = base64_session.encode("utf-8") + message_bytes = base64.b64decode(base64_bytes) + session = pickle.loads(message_bytes) # noqa: S301 + token = session.pkce_get_auth_token(url_redirect=url) + session.process_auth_token(token) + return session + + return await asyncio.to_thread(inner) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + assert values is not None + + if action == CONF_ACTION_START_PKCE_LOGIN: + async with AuthenticationHelper(mass, cast(str, values["session_id"])) as auth_helper: + quality = str(values.get(CONF_QUALITY)) + base64_session = await tidal_auth_url(auth_helper, quality) + values[CONF_TEMP_SESSION] = base64_session + # Tidal is (ab)using the AuthenticationHelper just to send the user to an URL + # there is no actual oauth callback happening, instead the user is redirected + # to a non-existent page and needs to copy the URL from the browser and paste it + # we simply wait here to allow the user to start the auth + await asyncio.sleep(15) + + if action == CONF_ACTION_COMPLETE_PKCE_LOGIN: + quality = str(values.get(CONF_QUALITY)) + pkce_url = str(values.get(CONF_OOPS_URL)) + base64_session = str(values.get(CONF_TEMP_SESSION)) + tidal_session = await tidal_pkce_login(base64_session, pkce_url) + if not tidal_session.check_login(): + msg = "Authentication to Tidal failed" + raise LoginFailed(msg) + # set the retrieved token on the values object to pass along + values[CONF_AUTH_TOKEN] = tidal_session.access_token + values[CONF_REFRESH_TOKEN] = tidal_session.refresh_token + values[CONF_EXPIRY_TIME] = tidal_session.expiry_time.isoformat() + values[CONF_USER_ID] = str(tidal_session.user.id) + values[CONF_TEMP_SESSION] = "" + + if action == CONF_ACTION_CLEAR_AUTH: + values[CONF_AUTH_TOKEN] = None + + if values.get(CONF_AUTH_TOKEN): + auth_entries: tuple[ConfigEntry, ...] = ( + ConfigEntry( + key="label_ok", + type=ConfigEntryType.LABEL, + label="You are authenticated with Tidal", + ), + ConfigEntry( + key=CONF_ACTION_CLEAR_AUTH, + type=ConfigEntryType.ACTION, + label="Reset authentication", + description="Reset the authentication for Tidal", + action=CONF_ACTION_CLEAR_AUTH, + value=None, + ), + ConfigEntry( + key=CONF_QUALITY, + type=ConfigEntryType.STRING, + label=CONF_QUALITY, + required=True, + hidden=True, + default_value=values.get(CONF_QUALITY, TidalQualityEnum.HI_RES.value), + value=values.get(CONF_QUALITY), + ), + ) + else: + auth_entries = ( + ConfigEntry( + key=CONF_QUALITY, + type=ConfigEntryType.STRING, + label="Quality setting for Tidal:", + required=True, + description="HIGH_LOSSLESS = 16bit 44.1kHz, HI_RES = Up to 24bit 192kHz", + options=tuple(ConfigValueOption(x.value, x.name) for x in TidalQualityEnum), + default_value=TidalQualityEnum.HI_RES.value, + value=values.get(CONF_QUALITY) if values else None, + ), + ConfigEntry( + key=LABEL_START_PKCE_LOGIN, + type=ConfigEntryType.LABEL, + label="The button below will redirect you to Tidal.com to authenticate.\n\n" + " After authenticating, you will be redirected to a page that prominently displays" + " 'Oops' at the top. That is normal, you need to copy that URL from the " + "address bar and come back here", + hidden=action == CONF_ACTION_START_PKCE_LOGIN, + ), + ConfigEntry( + key=CONF_ACTION_START_PKCE_LOGIN, + type=ConfigEntryType.ACTION, + label="Starts the auth process via PKCE on Tidal.com", + description="This button will redirect you to Tidal.com to authenticate." + " After authenticating, you will be redirected to a page that prominently displays" + " 'Oops' at the top.", + action=CONF_ACTION_START_PKCE_LOGIN, + depends_on=CONF_QUALITY, + action_label="Starts the auth process via PKCE on Tidal.com", + value=values.get(CONF_TEMP_SESSION) if values else None, + hidden=action == CONF_ACTION_START_PKCE_LOGIN, + ), + ConfigEntry( + key=CONF_TEMP_SESSION, + type=ConfigEntryType.STRING, + label="Temporary session for Tidal", + hidden=True, + required=False, + value=values.get(CONF_TEMP_SESSION) if values else None, + ), + ConfigEntry( + key=LABEL_OOPS_URL, + type=ConfigEntryType.LABEL, + label="Copy the URL from the 'Oops' page that you were previously redirected to" + " and paste it in the field below", + hidden=action != CONF_ACTION_START_PKCE_LOGIN, + ), + ConfigEntry( + key=CONF_OOPS_URL, + type=ConfigEntryType.STRING, + label="Oops URL from Tidal redirect", + description="This field should be filled manually by you after authenticating on" + " Tidal.com and being redirected to a page that prominently displays" + " 'Oops' at the top.", + depends_on=CONF_ACTION_START_PKCE_LOGIN, + value=values.get(CONF_OOPS_URL) if values else None, + hidden=action != CONF_ACTION_START_PKCE_LOGIN, + ), + ConfigEntry( + key=LABEL_COMPLETE_PKCE_LOGIN, + type=ConfigEntryType.LABEL, + label="After pasting the URL in the field above, click the button below to complete" + " the process.", + hidden=action != CONF_ACTION_START_PKCE_LOGIN, + ), + ConfigEntry( + key=CONF_ACTION_COMPLETE_PKCE_LOGIN, + type=ConfigEntryType.ACTION, + label="Complete the auth process via PKCE on Tidal.com", + description="Click this after adding the 'Oops' URL above, this will complete the" + " authentication process.", + action=CONF_ACTION_COMPLETE_PKCE_LOGIN, + depends_on=CONF_OOPS_URL, + action_label="Complete the auth process via PKCE on Tidal.com", + value=None, + hidden=action != CONF_ACTION_START_PKCE_LOGIN, + ), + ) + + # return the collected config entries + return ( + *auth_entries, + ConfigEntry( + key=CONF_AUTH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Authentication token for Tidal", + description="You need to link Music Assistant to your Tidal account.", + hidden=True, + value=values.get(CONF_AUTH_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_REFRESH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Refresh token for Tidal", + description="You need to link Music Assistant to your Tidal account.", + hidden=True, + value=values.get(CONF_REFRESH_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_EXPIRY_TIME, + type=ConfigEntryType.STRING, + label="Expiry time of auth token for Tidal", + hidden=True, + value=values.get(CONF_EXPIRY_TIME) if values else None, + ), + ConfigEntry( + key=CONF_USER_ID, + type=ConfigEntryType.STRING, + label="Your Tidal User ID", + description="This is your unique Tidal user ID.", + hidden=True, + value=values.get(CONF_USER_ID) if values else None, + ), + ) + + +class TidalProvider(MusicProvider): + """Implementation of a Tidal MusicProvider.""" + + _tidal_session: TidalSession | None = None + _tidal_user_id: str + # rate limiter needs to be specified on provider-level, + # so make it an instance attribute + throttler = ThrottlerManager(rate_limit=1, period=2) + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self._tidal_user_id = str(self.config.get_value(CONF_USER_ID)) + try: + self._tidal_session = await self._get_tidal_session() + except Exception as err: + if "401 Client Error: Unauthorized" in str(err): + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_AUTH_TOKEN, None + ) + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_REFRESH_TOKEN, None + ) + raise LoginFailed("Credentials expired, you need to re-setup") + raise + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SEARCH, + ProviderFeature.LIBRARY_ARTISTS_EDIT, + ProviderFeature.LIBRARY_ALBUMS_EDIT, + ProviderFeature.LIBRARY_TRACKS_EDIT, + ProviderFeature.LIBRARY_PLAYLISTS_EDIT, + ProviderFeature.PLAYLIST_CREATE, + ProviderFeature.SIMILAR_TRACKS, + ProviderFeature.BROWSE, + ProviderFeature.PLAYLIST_TRACKS_EDIT, + ) + + async def search( + self, + search_query: str, + media_types: list[MediaType], + limit: int = 5, + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. + :param limit: Number of items to return in the search (per type). + """ + parsed_results = SearchResults() + media_types = [ + x + for x in media_types + if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return parsed_results + + tidal_session = await self._get_tidal_session() + search_query = search_query.replace("'", "") + results = await search(tidal_session, search_query, media_types, limit) + + if results["artists"]: + parsed_results.artists = [self._parse_artist(artist) for artist in results["artists"]] + if results["albums"]: + parsed_results.albums = [self._parse_album(album) for album in results["albums"]] + if results["playlists"]: + parsed_results.playlists = [ + self._parse_playlist(playlist) for playlist in results["playlists"] + ] + if results["tracks"]: + parsed_results.tracks = [self._parse_track(track) for track in results["tracks"]] + return parsed_results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Tidal.""" + tidal_session = await self._get_tidal_session() + artist: TidalArtist # satisfy the type checker + async for artist in self._iter_items( + get_library_artists, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT + ): + yield self._parse_artist(artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Tidal.""" + tidal_session = await self._get_tidal_session() + album: TidalAlbum # satisfy the type checker + async for album in self._iter_items( + get_library_albums, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT + ): + yield self._parse_album(album) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Tidal.""" + tidal_session = await self._get_tidal_session() + track: TidalTrack # satisfy the type checker + async for track in self._iter_items( + get_library_tracks, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT + ): + yield self._parse_track(track) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + tidal_session = await self._get_tidal_session() + playlist: TidalPlaylist # satisfy the type checker + async for playlist in self._iter_items( + get_library_playlists, tidal_session, self._tidal_user_id + ): + yield self._parse_playlist(playlist) + + @throttle_with_retries + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + tidal_session = await self._get_tidal_session() + tracks_obj = await get_album_tracks(tidal_session, prov_album_id) + return [self._parse_track(track_obj=track_obj) for track_obj in tracks_obj] + + @throttle_with_retries + async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: + """Get a list of all albums for the given artist.""" + tidal_session = await self._get_tidal_session() + artist_albums_obj = await get_artist_albums(tidal_session, prov_artist_id) + return [self._parse_album(album) for album in artist_albums_obj] + + @throttle_with_retries + async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: + """Get a list of 10 most popular tracks for the given artist.""" + tidal_session = await self._get_tidal_session() + try: + artist_toptracks_obj = await get_artist_toptracks(tidal_session, prov_artist_id) + return [self._parse_track(track) for track in artist_toptracks_obj] + except tidal_exceptions.ObjectNotFound as err: + self.logger.warning(f"Failed to get toptracks for artist {prov_artist_id}: {err}") + return [] + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Get playlist tracks.""" + tidal_session = await self._get_tidal_session() + result: list[Track] = [] + page_size = 200 + offset = page * page_size + track_obj: TidalTrack # satisfy the type checker + tidal_tracks = await get_playlist_tracks( + tidal_session, prov_playlist_id, limit=page_size, offset=offset + ) + for index, track_obj in enumerate(tidal_tracks, 1): + track = self._parse_track(track_obj=track_obj) + track.position = offset + index + result.append(track) + return result + + @throttle_with_retries + async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: + """Get similar tracks for given track id.""" + tidal_session = await self._get_tidal_session() + similar_tracks_obj = await get_similar_tracks(tidal_session, prov_track_id, limit) + return [self._parse_track(track) for track in similar_tracks_obj] + + async def library_add(self, item: MediaItemType) -> bool: + """Add item to library.""" + tidal_session = await self._get_tidal_session() + return await library_items_add_remove( + tidal_session, + str(self._tidal_user_id), + item.item_id, + item.media_type, + add=True, + ) + + async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: + """Remove item from library.""" + tidal_session = await self._get_tidal_session() + return await library_items_add_remove( + tidal_session, + str(self._tidal_user_id), + prov_item_id, + media_type, + add=False, + ) + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + tidal_session = await self._get_tidal_session() + return await add_playlist_tracks(tidal_session, prov_playlist_id, prov_track_ids) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + prov_track_ids = [] + tidal_session = await self._get_tidal_session() + for pos in positions_to_remove: + for tidal_track in await get_playlist_tracks( + tidal_session, prov_playlist_id, limit=1, offset=pos - 1 + ): + prov_track_ids.append(tidal_track.id) + return await remove_playlist_tracks(tidal_session, prov_playlist_id, prov_track_ids) + + async def create_playlist(self, name: str) -> Playlist: + """Create a new playlist on provider with given name.""" + tidal_session = await self._get_tidal_session() + playlist_obj = await create_playlist( + session=tidal_session, + user_id=str(self._tidal_user_id), + title=name, + description="", + ) + return self._parse_playlist(playlist_obj) + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + tidal_session = await self._get_tidal_session() + # make sure a valid track is requested. + if not (track := await get_track(tidal_session, item_id)): + msg = f"track {item_id} not found" + raise MediaNotFoundError(msg) + stream: TidalStream = await get_stream(track) + manifest = stream.get_stream_manifest() + if stream.is_mpd: + # for mpeg-dash streams we just pass the complete base64 manifest + url = f"data:application/dash+xml;base64,{manifest.manifest}" + else: + # as far as I can oversee a BTS stream is just a single URL + url = manifest.urls[0] + + return StreamDetails( + item_id=track.id, + provider=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(manifest.codecs), + sample_rate=manifest.sample_rate, + bit_depth=stream.bit_depth, + channels=2, + ), + stream_type=StreamType.HTTP, + duration=track.duration, + path=url, + ) + + @throttle_with_retries + async def get_artist(self, prov_artist_id: str) -> Artist: + """Get artist details for given artist id.""" + tidal_session = await self._get_tidal_session() + try: + artist_obj = await get_artist(tidal_session, prov_artist_id) + return self._parse_artist(artist_obj) + except tidal_exceptions.ObjectNotFound as err: + raise MediaNotFoundError from err + + @throttle_with_retries + async def get_album(self, prov_album_id: str) -> Album: + """Get album details for given album id.""" + tidal_session = await self._get_tidal_session() + try: + album_obj = await get_album(tidal_session, prov_album_id) + return self._parse_album(album_obj) + except tidal_exceptions.ObjectNotFound as err: + raise MediaNotFoundError from err + + @throttle_with_retries + async def get_track(self, prov_track_id: str) -> Track: + """Get track details for given track id.""" + tidal_session = await self._get_tidal_session() + track_obj = await get_track(tidal_session, prov_track_id) + try: + track = self._parse_track(track_obj) + # get some extra details for the full track info + with suppress(tidal_exceptions.MetadataNotAvailable, AttributeError): + lyrics: TidalLyrics = await asyncio.to_thread(track_obj.lyrics) + track.metadata.lyrics = lyrics.text + return track + except tidal_exceptions.ObjectNotFound as err: + raise MediaNotFoundError from err + + @throttle_with_retries + async def get_playlist(self, prov_playlist_id: str) -> Playlist: + """Get playlist details for given playlist id.""" + tidal_session = await self._get_tidal_session() + playlist_obj = await get_playlist(tidal_session, prov_playlist_id) + return self._parse_playlist(playlist_obj) + + def get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: + """Create a generic item mapping.""" + return ItemMapping( + media_type=media_type, + item_id=key, + provider=self.instance_id, + name=name, + ) + + async def _get_tidal_session(self) -> TidalSession: + """Ensure the current token is valid and return a tidal session.""" + if ( + self._tidal_session + and self._tidal_session.access_token + and datetime.fromisoformat(str(self.config.get_value(CONF_EXPIRY_TIME))) + > (datetime.now() + timedelta(days=1)) + ): + return self._tidal_session + self._tidal_session = await self._load_tidal_session( + token_type="Bearer", + quality=str(self.config.get_value(CONF_QUALITY)), + access_token=str(self.config.get_value(CONF_AUTH_TOKEN)), + refresh_token=str(self.config.get_value(CONF_REFRESH_TOKEN)), + expiry_time=datetime.fromisoformat(str(self.config.get_value(CONF_EXPIRY_TIME))), + ) + self.mass.config.set_raw_provider_config_value( + self.config.instance_id, + CONF_AUTH_TOKEN, + self._tidal_session.access_token, + encrypted=True, + ) + self.mass.config.set_raw_provider_config_value( + self.config.instance_id, + CONF_REFRESH_TOKEN, + self._tidal_session.refresh_token, + encrypted=True, + ) + self.mass.config.set_raw_provider_config_value( + self.config.instance_id, + CONF_EXPIRY_TIME, + self._tidal_session.expiry_time.isoformat(), + ) + return self._tidal_session + + async def _load_tidal_session( + self, + token_type: str, + quality: str, + access_token: str, + refresh_token: str, + expiry_time: datetime | None = None, + ) -> TidalSession: + """Load the tidalapi Session.""" + + def inner() -> TidalSession: + config = TidalConfig(quality=quality, item_limit=10000, alac=False) + session = TidalSession(config=config) + session.load_oauth_session( + token_type=token_type, + access_token=access_token, + refresh_token=refresh_token, + expiry_time=expiry_time, + is_pkce=True, + ) + return session + + return await asyncio.to_thread(inner) + + # Parsers + + def _parse_artist(self, artist_obj: TidalArtist) -> Artist: + """Parse tidal artist object to generic layout.""" + artist_id = artist_obj.id + artist = Artist( + item_id=str(artist_id), + provider=self.instance_id, + name=artist_obj.name, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + # NOTE: don't use the /browse endpoint as it's + # not working for musicbrainz lookups + url=f"https://tidal.com/artist/{artist_id}", + ) + }, + ) + # metadata + if artist_obj.picture: + picture_id = artist_obj.picture.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + artist.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + + return artist + + def _parse_album(self, album_obj: TidalAlbum) -> Album: + """Parse tidal album object to generic layout.""" + name = album_obj.name + version = album_obj.version or "" + album_id = album_obj.id + album = Album( + item_id=str(album_id), + provider=self.instance_id, + name=name, + version=version, + provider_mappings={ + ProviderMapping( + item_id=str(album_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.FLAC, + ), + url=f"https://tidal.com/album/{album_id}", + available=album_obj.available, + ) + }, + ) + various_artist_album: bool = False + for artist_obj in album_obj.artists: + if artist_obj.name == "Various Artists": + various_artist_album = True + album.artists.append(self._parse_artist(artist_obj)) + + if album_obj.type == "COMPILATION" or various_artist_album: + album.album_type = AlbumType.COMPILATION + elif album_obj.type == "ALBUM": + album.album_type = AlbumType.ALBUM + elif album_obj.type == "EP": + album.album_type = AlbumType.EP + elif album_obj.type == "SINGLE": + album.album_type = AlbumType.SINGLE + + album.year = int(album_obj.year) + # metadata + if album_obj.universal_product_number: + album.external_ids.add((ExternalID.BARCODE, album_obj.universal_product_number)) + album.metadata.copyright = album_obj.copyright + album.metadata.explicit = album_obj.explicit + album.metadata.popularity = album_obj.popularity + if album_obj.cover: + picture_id = album_obj.cover.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + album.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + + return album + + def _parse_track( + self, + track_obj: TidalTrack, + ) -> Track: + """Parse tidal track object to generic layout.""" + version = track_obj.version or "" + track_id = str(track_obj.id) + track = Track( + item_id=str(track_id), + provider=self.instance_id, + name=track_obj.name, + version=version, + duration=track_obj.duration, + provider_mappings={ + ProviderMapping( + item_id=str(track_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.FLAC, + bit_depth=24 if track_obj.is_hi_res_lossless else 16, + ), + url=f"https://tidal.com/track/{track_id}", + available=track_obj.available, + ) + }, + disc_number=track_obj.volume_num or 0, + track_number=track_obj.track_num or 0, + ) + if track_obj.isrc: + track.external_ids.add((ExternalID.ISRC, track_obj.isrc)) + track.artists = UniqueList() + for track_artist in track_obj.artists: + artist = self._parse_artist(track_artist) + track.artists.append(artist) + # metadata + track.metadata.explicit = track_obj.explicit + track.metadata.popularity = track_obj.popularity + track.metadata.copyright = track_obj.copyright + if track_obj.album: + # Here we use an ItemMapping as Tidal returns + # minimal data when getting an Album from a Track + track.album = self.get_item_mapping( + media_type=MediaType.ALBUM, + key=str(track_obj.album.id), + name=track_obj.album.name, + ) + if track_obj.album.cover: + picture_id = track_obj.album.cover.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + track.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + return track + + def _parse_playlist(self, playlist_obj: TidalPlaylist) -> Playlist: + """Parse tidal playlist object to generic layout.""" + playlist_id = playlist_obj.id + creator_id = playlist_obj.creator.id if playlist_obj.creator else None + creator_name = playlist_obj.creator.name if playlist_obj.creator else "Tidal" + playlist = Playlist( + item_id=str(playlist_id), + provider=self.instance_id, + name=playlist_obj.name, + owner=creator_name, + provider_mappings={ + ProviderMapping( + item_id=str(playlist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f"{BROWSE_URL}/playlist/{playlist_id}", + ) + }, + ) + is_editable = bool(creator_id and str(creator_id) == self._tidal_user_id) + playlist.is_editable = is_editable + # metadata + playlist.cache_checksum = str(playlist_obj.last_updated) + playlist.metadata.popularity = playlist_obj.popularity + if picture := (playlist_obj.square_picture or playlist_obj.picture): + picture_id = picture.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + playlist.metadata.images = UniqueList( + [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + ) + + return playlist + + async def _iter_items( + self, + func: Callable[_P, list[_R]] | Callable[_P, Awaitable[list[_R]]], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> AsyncGenerator[_R, None]: + """Yield all items from a larger listing.""" + offset = 0 + while True: + if asyncio.iscoroutinefunction(func): + chunk = await func(*args, **kwargs, offset=offset) # type: ignore[arg-type] + else: + chunk = await asyncio.to_thread(func, *args, **kwargs, offset=offset) # type: ignore[arg-type] + offset += len(chunk) + for item in chunk: + yield item + if len(chunk) < DEFAULT_LIMIT: + break + + async def _get_media_info( + self, item_id: str, url: str, force_refresh: bool = False + ) -> AudioTags: + """Retrieve (cached) mediainfo for track.""" + cache_category = CacheCategory.MEDIA_INFO + cache_base_key = self.lookup_key + # do we have some cached info for this url ? + cached_info = await self.mass.cache.get( + item_id, category=cache_category, base_key=cache_base_key + ) + if cached_info and not force_refresh: + media_info = AudioTags.parse(cached_info) + else: + # parse info with ffprobe (and store in cache) + media_info = await parse_tags(url) + await self.mass.cache.set( + item_id, + media_info.raw, + category=cache_category, + base_key=cache_base_key, + ) + return media_info diff --git a/music_assistant/providers/tidal/helpers.py b/music_assistant/providers/tidal/helpers.py new file mode 100644 index 00000000..9dba4d70 --- /dev/null +++ b/music_assistant/providers/tidal/helpers.py @@ -0,0 +1,388 @@ +"""Helper module for parsing the Tidal API. + +This helpers file is an async wrapper around the excellent tidalapi package. +While the tidalapi package does an excellent job at parsing the Tidal results, +it is unfortunately not async, which is required for Music Assistant to run smoothly. +This also nicely separates the parsing logic from the Tidal provider logic. + +CREDITS: +tidalapi: https://github.com/tamland/python-tidal +""" + +import asyncio +import logging + +from music_assistant_models.enums import MediaType +from music_assistant_models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable +from tidalapi import Album as TidalAlbum +from tidalapi import Artist as TidalArtist +from tidalapi import Favorites as TidalFavorites +from tidalapi import LoggedInUser +from tidalapi import Playlist as TidalPlaylist +from tidalapi import Session as TidalSession +from tidalapi import Track as TidalTrack +from tidalapi import UserPlaylist as TidalUserPlaylist +from tidalapi.exceptions import MetadataNotAvailable, ObjectNotFound, TooManyRequests +from tidalapi.media import Stream as TidalStream + +DEFAULT_LIMIT = 50 +LOGGER = logging.getLogger(__name__) + + +async def get_library_artists( + session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 +) -> list[TidalArtist]: + """Async wrapper around the tidalapi Favorites.artists function.""" + + def inner() -> list[TidalArtist]: + artists: list[TidalArtist] = TidalFavorites(session, user_id).artists( + limit=limit, offset=offset + ) + return artists + + return await asyncio.to_thread(inner) + + +async def library_items_add_remove( + session: TidalSession, + user_id: str, + item_id: str, + media_type: MediaType, + add: bool = True, +) -> bool: + """Async wrapper around the tidalapi Favorites.items add/remove function.""" + + def inner() -> bool: + tidal_favorites = TidalFavorites(session, user_id) + if media_type == MediaType.UNKNOWN: + return False + response: bool = False + if add: + match media_type: + case MediaType.ARTIST: + response = tidal_favorites.add_artist(item_id) + case MediaType.ALBUM: + response = tidal_favorites.add_album(item_id) + case MediaType.TRACK: + response = tidal_favorites.add_track(item_id) + case MediaType.PLAYLIST: + response = tidal_favorites.add_playlist(item_id) + else: + match media_type: + case MediaType.ARTIST: + response = tidal_favorites.remove_artist(item_id) + case MediaType.ALBUM: + response = tidal_favorites.remove_album(item_id) + case MediaType.TRACK: + response = tidal_favorites.remove_track(item_id) + case MediaType.PLAYLIST: + response = tidal_favorites.remove_playlist(item_id) + return response + + return await asyncio.to_thread(inner) + + +async def get_artist(session: TidalSession, prov_artist_id: str) -> TidalArtist: + """Async wrapper around the tidalapi Artist function.""" + + def inner() -> TidalArtist: + try: + return TidalArtist(session, prov_artist_id) + except ObjectNotFound as err: + msg = f"Artist {prov_artist_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_artist_albums(session: TidalSession, prov_artist_id: str) -> list[TidalAlbum]: + """Async wrapper around 3 tidalapi album functions.""" + + def inner() -> list[TidalAlbum]: + try: + artist_obj = TidalArtist(session, prov_artist_id) + except ObjectNotFound as err: + msg = f"Artist {prov_artist_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + else: + all_albums: list[TidalAlbum] = artist_obj.get_albums(limit=DEFAULT_LIMIT) + # extend with EPs and singles + all_albums.extend(artist_obj.get_ep_singles(limit=DEFAULT_LIMIT)) + # extend with compilations + # note that the Tidal API gives back really strange results here so + # filter on either various artists or the artist id + for album in artist_obj.get_other(limit=DEFAULT_LIMIT): + if album.artist.id == artist_obj.id or album.artist.name == "Various Artists": + all_albums.append(album) + return all_albums + + return await asyncio.to_thread(inner) + + +async def get_artist_toptracks( + session: TidalSession, prov_artist_id: str, limit: int = 10, offset: int = 0 +) -> list[TidalTrack]: + """Async wrapper around the tidalapi Artist.get_top_tracks function.""" + + def inner() -> list[TidalTrack]: + top_tracks: list[TidalTrack] = TidalArtist(session, prov_artist_id).get_top_tracks( + limit=limit, offset=offset + ) + return top_tracks + + return await asyncio.to_thread(inner) + + +async def get_library_albums( + session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 +) -> list[TidalAlbum]: + """Async wrapper around the tidalapi Favorites.albums function.""" + + def inner() -> list[TidalAlbum]: + albums: list[TidalAlbum] = TidalFavorites(session, user_id).albums( + limit=limit, offset=offset + ) + return albums + + return await asyncio.to_thread(inner) + + +async def get_album(session: TidalSession, prov_album_id: str) -> TidalAlbum: + """Async wrapper around the tidalapi Album function.""" + + def inner() -> TidalAlbum: + try: + return TidalAlbum(session, prov_album_id) + except ObjectNotFound as err: + msg = f"Album {prov_album_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_track(session: TidalSession, prov_track_id: str) -> TidalTrack: + """Async wrapper around the tidalapi Track function.""" + + def inner() -> TidalTrack: + try: + return TidalTrack(session, prov_track_id) + except ObjectNotFound as err: + msg = f"Track {prov_track_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_stream(track: TidalTrack) -> TidalStream: + """Async wrapper around the tidalapi Track.get_stream_url function.""" + + def inner() -> TidalStream: + try: + return track.get_stream() + except ObjectNotFound as err: + msg = f"Track {track.id} has no available stream" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_track_url(session: TidalSession, prov_track_id: str) -> str: + """Async wrapper around the tidalapi Track.get_url function.""" + + def inner() -> str: + try: + track_url: str = TidalTrack(session, prov_track_id).get_url() + return track_url + except ObjectNotFound as err: + msg = f"Track {prov_track_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_album_tracks(session: TidalSession, prov_album_id: str) -> list[TidalTrack]: + """Async wrapper around the tidalapi Album.tracks function.""" + + def inner() -> list[TidalTrack]: + try: + tracks: list[TidalTrack] = TidalAlbum(session, prov_album_id).tracks( + limit=DEFAULT_LIMIT + ) + return tracks + except ObjectNotFound as err: + msg = f"Album {prov_album_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_library_tracks( + session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 +) -> list[TidalTrack]: + """Async wrapper around the tidalapi Favorites.tracks function.""" + + def inner() -> list[TidalTrack]: + tracks: list[TidalTrack] = TidalFavorites(session, user_id).tracks( + limit=limit, offset=offset + ) + return tracks + + return await asyncio.to_thread(inner) + + +async def get_library_playlists( + session: TidalSession, user_id: str, offset: int = 0 +) -> list[TidalPlaylist]: + """Async wrapper around the tidalapi LoggedInUser.playlist_and_favorite_playlists function.""" + + def inner() -> list[TidalPlaylist]: + playlists: list[TidalPlaylist] = LoggedInUser( + session, user_id + ).playlist_and_favorite_playlists(offset=offset) + return playlists + + return await asyncio.to_thread(inner) + + +async def get_playlist(session: TidalSession, prov_playlist_id: str) -> TidalPlaylist: + """Async wrapper around the tidal Playlist function.""" + + def inner() -> TidalPlaylist: + try: + return TidalPlaylist(session, prov_playlist_id) + except ObjectNotFound as err: + msg = f"Playlist {prov_playlist_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def get_playlist_tracks( + session: TidalSession, + prov_playlist_id: str, + limit: int = DEFAULT_LIMIT, + offset: int = 0, +) -> list[TidalTrack]: + """Async wrapper around the tidal Playlist.tracks function.""" + + def inner() -> list[TidalTrack]: + try: + tracks: list[TidalTrack] = TidalPlaylist(session, prov_playlist_id).tracks( + limit=limit, offset=offset + ) + return tracks + except ObjectNotFound as err: + msg = f"Playlist {prov_playlist_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def add_playlist_tracks( + session: TidalSession, prov_playlist_id: str, track_ids: list[str] +) -> None: + """Async wrapper around the tidal Playlist.add function.""" + + def inner() -> None: + TidalUserPlaylist(session, prov_playlist_id).add(track_ids) + + return await asyncio.to_thread(inner) + + +async def remove_playlist_tracks( + session: TidalSession, prov_playlist_id: str, track_ids: list[str] +) -> None: + """Async wrapper around the tidal Playlist.remove function.""" + + def inner() -> None: + for item in track_ids: + TidalUserPlaylist(session, prov_playlist_id).remove_by_id(int(item)) + + return await asyncio.to_thread(inner) + + +async def create_playlist( + session: TidalSession, user_id: str, title: str, description: str | None = None +) -> TidalPlaylist: + """Async wrapper around the tidal LoggedInUser.create_playlist function.""" + + def inner() -> TidalPlaylist: + playlist: TidalPlaylist = LoggedInUser(session, user_id).create_playlist(title, description) + return playlist + + return await asyncio.to_thread(inner) + + +async def get_similar_tracks( + session: TidalSession, prov_track_id: str, limit: int = 25 +) -> list[TidalTrack]: + """Async wrapper around the tidal Track.get_similar_tracks function.""" + + def inner() -> list[TidalTrack]: + try: + tracks: list[TidalTrack] = TidalTrack(session, prov_track_id).get_track_radio( + limit=limit + ) + return tracks + except (MetadataNotAvailable, ObjectNotFound) as err: + msg = f"Track {prov_track_id} not found" + raise MediaNotFoundError(msg) from err + except TooManyRequests: + msg = "Tidal API rate limit reached" + raise ResourceTemporarilyUnavailable(msg) + + return await asyncio.to_thread(inner) + + +async def search( + session: TidalSession, + query: str, + media_types: list[MediaType], + limit: int = 50, + offset: int = 0, +) -> dict[str, str]: + """Async wrapper around the tidalapi Search function.""" + + def inner() -> dict[str, str]: + search_types = [] + if MediaType.ARTIST in media_types: + search_types.append(TidalArtist) + if MediaType.ALBUM in media_types: + search_types.append(TidalAlbum) + if MediaType.TRACK in media_types: + search_types.append(TidalTrack) + if MediaType.PLAYLIST in media_types: + search_types.append(TidalPlaylist) + + models = search_types + results: dict[str, str] = session.search(query, models, limit, offset) + return results + + return await asyncio.to_thread(inner) diff --git a/music_assistant/providers/tidal/icon.svg b/music_assistant/providers/tidal/icon.svg new file mode 100644 index 00000000..8bf8e023 --- /dev/null +++ b/music_assistant/providers/tidal/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/music_assistant/providers/tidal/icon_dark.svg b/music_assistant/providers/tidal/icon_dark.svg new file mode 100644 index 00000000..889198f7 --- /dev/null +++ b/music_assistant/providers/tidal/icon_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/music_assistant/providers/tidal/manifest.json b/music_assistant/providers/tidal/manifest.json new file mode 100644 index 00000000..d21ddd5a --- /dev/null +++ b/music_assistant/providers/tidal/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "tidal", + "name": "Tidal", + "description": "Support for the Tidal streaming provider in Music Assistant.", + "codeowners": ["@jozefKruszynski"], + "requirements": ["tidalapi==0.8.0"], + "documentation": "https://music-assistant.io/music-providers/tidal/", + "multi_instance": true +} diff --git a/music_assistant/providers/tunein/__init__.py b/music_assistant/providers/tunein/__init__.py new file mode 100644 index 00000000..4e605c71 --- /dev/null +++ b/music_assistant/providers/tunein/__init__.py @@ -0,0 +1,295 @@ +"""Tune-In musicprovider support for MusicAssistant.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import InvalidDataError, LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( + AudioFormat, + ContentType, + ImageType, + MediaItemImage, + MediaType, + ProviderMapping, + Radio, +) +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.constants import CONF_USERNAME +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.music_provider import MusicProvider + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_RADIOS, + ProviderFeature.BROWSE, +) + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + if not config.get_value(CONF_USERNAME): + msg = "Username is invalid" + raise LoginFailed(msg) + + return TuneInProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + # ruff: noqa: ARG001 + return ( + ConfigEntry( + key=CONF_USERNAME, + type=ConfigEntryType.STRING, + label="Username", + required=True, + ), + ) + + +class TuneInProvider(MusicProvider): + """Provider implementation for Tune In.""" + + _throttler: Throttler + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + self._throttler = Throttler(rate_limit=1, period=2) + if "@" in self.config.get_value(CONF_USERNAME): + self.logger.warning( + "Email address detected instead of username, " + "it is advised to use the tunein username instead of email." + ) + + async def get_library_radios(self) -> AsyncGenerator[Radio, None]: + """Retrieve library/subscribed radio stations from the provider.""" + + async def parse_items( + items: list[dict], folder: str | None = None + ) -> AsyncGenerator[Radio, None]: + for item in items: + item_type = item.get("type", "") + if "unavailable" in item.get("key", ""): + continue + if not item.get("is_available", True): + continue + if item_type == "audio": + if "preset_id" not in item: + continue + # each radio station can have multiple streams add each one as different quality + stream_info = await self._get_stream_info(item["preset_id"]) + yield self._parse_radio(item, stream_info, folder) + elif item_type == "link" and item.get("item") == "url": + # custom url + try: + yield self._parse_radio(item) + except InvalidDataError as err: + # there may be invalid custom urls, ignore those + self.logger.warning(str(err)) + elif item_type == "link": + # stations are in sublevel (new style) + if sublevel := await self.__get_data(item["URL"], render="json"): + async for subitem in parse_items(sublevel["body"], item["text"]): + yield subitem + elif item.get("children"): + # stations are in sublevel (old style ?) + async for subitem in parse_items(item["children"], item["text"]): + yield subitem + + data = await self.__get_data("Browse.ashx", c="presets") + if data and "body" in data: + async for item in parse_items(data["body"]): + yield item + + async def get_radio(self, prov_radio_id: str) -> Radio: + """Get radio station details.""" + if not prov_radio_id.startswith("http"): + if "--" in prov_radio_id: + prov_radio_id, media_type = prov_radio_id.split("--", 1) + else: + media_type = None + params = {"c": "composite", "detail": "listing", "id": prov_radio_id} + result = await self.__get_data("Describe.ashx", **params) + if result and result.get("body") and result["body"][0].get("children"): + item = result["body"][0]["children"][0] + stream_info = await self._get_stream_info(prov_radio_id) + for stream in stream_info: + if media_type and stream["media_type"] != media_type: + continue + return self._parse_radio(item, [stream]) + # fallback - e.g. for handle custom urls ... + async for radio in self.get_library_radios(): + if radio.item_id == prov_radio_id: + return radio + msg = f"Item {prov_radio_id} not found" + raise MediaNotFoundError(msg) + + def _parse_radio( + self, details: dict, stream_info: list[dict] | None = None, folder: str | None = None + ) -> Radio: + """Parse Radio object from json obj returned from api.""" + if "name" in details: + name = details["name"] + else: + # parse name from text attr + name = details["text"] + if " | " in name: + name = name.split(" | ")[1] + name = name.split(" (")[0] + + if stream_info is not None: + # stream info is provided: parse stream objects into provider mappings + radio = Radio( + item_id=details["preset_id"], + provider=self.lookup_key, + name=name, + provider_mappings={ + ProviderMapping( + item_id=f'{details["preset_id"]}--{stream["media_type"]}', + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(stream["media_type"]), + bit_rate=stream.get("bitrate", 128), + ), + details=stream["url"], + available=details.get("is_available", True), + ) + for stream in stream_info + }, + ) + else: + # custom url (no stream object present) + radio = Radio( + item_id=details["URL"], + provider=self.lookup_key, + name=name, + provider_mappings={ + ProviderMapping( + item_id=details["URL"], + provider_domain=self.domain, + provider_instance=self.instance_id, + audio_format=AudioFormat( + content_type=ContentType.UNKNOWN, + ), + details=details["URL"], + available=details.get("is_available", True), + ) + }, + ) + + # preset number is used for sorting (not present at stream time) + preset_number = details.get("preset_number", 0) + radio.position = preset_number + if "text" in details: + radio.metadata.description = details["text"] + # image + if img := details.get("image") or details.get("logo"): + radio.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=img, + provider=self.lookup_key, + remotely_accessible=True, + ) + ] + return radio + + async def _get_stream_info(self, preset_id: str) -> list[dict]: + """Get stream info for a radio station.""" + cache_base_key = "tunein_stream" + if cache := await self.mass.cache.get(preset_id, base_key=cache_base_key): + return cache + result = (await self.__get_data("Tune.ashx", id=preset_id))["body"] + await self.mass.cache.set(preset_id, result, base_key=cache_base_key) + return result + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Get streamdetails for a radio station.""" + if item_id.startswith("http"): + # custom url + return StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=AudioFormat( + content_type=ContentType.UNKNOWN, + ), + media_type=MediaType.RADIO, + stream_type=StreamType.HTTP, + path=item_id, + can_seek=False, + ) + if "--" in item_id: + stream_item_id, media_type = item_id.split("--", 1) + else: + media_type = None + stream_item_id = item_id + for stream in await self._get_stream_info(stream_item_id): + if media_type and stream["media_type"] != media_type: + continue + return StreamDetails( + provider=self.domain, + item_id=item_id, + # set contenttype to unknown so ffmpeg can auto detect it + audio_format=AudioFormat(content_type=ContentType.UNKNOWN), + media_type=MediaType.RADIO, + stream_type=StreamType.HTTP, + path=stream["url"], + can_seek=False, + ) + msg = f"Unable to retrieve stream details for {item_id}" + raise MediaNotFoundError(msg) + + async def __get_data(self, endpoint: str, **kwargs): + """Get data from api.""" + if endpoint.startswith("http"): + url = endpoint + else: + url = f"https://opml.radiotime.com/{endpoint}" + kwargs["formats"] = "ogg,aac,wma,mp3,hls" + kwargs["username"] = self.config.get_value(CONF_USERNAME) + kwargs["partnerId"] = "1" + kwargs["render"] = "json" + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers = {"Accept-Language": f"{locale}, {language};q=0.9, *;q=0.5"} + async with ( + self._throttler, + self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, + ): + result = await response.json() + if not result or "error" in result: + self.logger.error(url) + self.logger.error(kwargs) + result = None + return result diff --git a/music_assistant/providers/tunein/icon.svg b/music_assistant/providers/tunein/icon.svg new file mode 100644 index 00000000..55cc9fee --- /dev/null +++ b/music_assistant/providers/tunein/icon.svg @@ -0,0 +1,4 @@ + diff --git a/music_assistant/providers/tunein/manifest.json b/music_assistant/providers/tunein/manifest.json new file mode 100644 index 00000000..57429700 --- /dev/null +++ b/music_assistant/providers/tunein/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "tunein", + "name": "Tune-In Radio", + "description": "Play your favorite radio stations from Tune-In in Music Assistant.", + "codeowners": ["@music-assistant"], + "requirements": [], + "documentation": "https://music-assistant.io/music-providers/tunein/", + "multi_instance": true +} diff --git a/music_assistant/providers/ytmusic/__init__.py b/music_assistant/providers/ytmusic/__init__.py new file mode 100644 index 00000000..75a62242 --- /dev/null +++ b/music_assistant/providers/ytmusic/__init__.py @@ -0,0 +1,859 @@ +"""Youtube Music support for MusicAssistant.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import AsyncGenerator +from time import time +from typing import TYPE_CHECKING, Any +from urllib.parse import unquote + +import yt_dlp +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import ( + InvalidDataError, + LoginFailed, + MediaNotFoundError, + UnplayableMediaError, +) +from music_assistant_models.media_items import ( + Album, + AlbumType, + Artist, + AudioFormat, + ContentType, + ImageType, + ItemMapping, + MediaItemImage, + MediaItemType, + MediaType, + Playlist, + ProviderMapping, + SearchResults, + Track, +) +from music_assistant_models.streamdetails import StreamDetails +from ytmusicapi.constants import SUPPORTED_LANGUAGES +from ytmusicapi.exceptions import YTMusicServerError + +from music_assistant.helpers import ( + add_remove_playlist_tracks, + get_album, + get_artist, + get_library_albums, + get_library_artists, + get_library_playlists, + get_library_tracks, + get_playlist, + get_song_radio_tracks, + get_track, + library_add_remove_album, + library_add_remove_artist, + library_add_remove_playlist, + login_oauth, + refresh_oauth_token, + search, +) +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.models.music_provider import MusicProvider + +if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + + +CONF_COOKIE = "cookie" +CONF_ACTION_AUTH = "auth" +CONF_AUTH_TOKEN = "auth_token" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_TOKEN_TYPE = "token_type" +CONF_EXPIRY_TIME = "expiry_time" + +YTM_DOMAIN = "https://music.youtube.com" +YTM_BASE_URL = f"{YTM_DOMAIN}/youtubei/v1/" +VARIOUS_ARTISTS_YTM_ID = "UCUTXlgdcKU5vfzFqHOWIvkA" +# Playlist ID's are not unique across instances for lists like 'Liked videos', 'SuperMix' etc. +# So we need to add a delimiter to make them unique +YT_PLAYLIST_ID_DELIMITER = "🎵" +YT_PERSONAL_PLAYLISTS = ( + "LM", # Liked songs + "SE" # Episodes for Later + "RDTMAK5uy_kset8DisdE7LSD4TNjEVvrKRTmG7a56sY", # SuperMix + "RDTMAK5uy_nGQKSMIkpr4o9VI_2i56pkGliD6FQRo50", # My Mix 1 + "RDTMAK5uy_lz2owBgwWf1mjzyn_NbxzMViQzIg8IAIg", # My Mix 2 + "RDTMAK5uy_k5UUl0lmrrfrjMpsT0CoMpdcBz1ruAO1k", # My Mix 3 + "RDTMAK5uy_nTsa0Irmcu2li2-qHBoZxtrpG9HuC3k_Q", # My Mix 4 + "RDTMAK5uy_lfZhS7zmIcmUhsKtkWylKzc0EN0LW90-s", # My Mix 5 + "RDTMAK5uy_k78ni6Y4fyyl0r2eiKkBEICh9Q5wJdfXk", # My Mix 6 + "RDTMAK5uy_lfhhWWw9v71CPrR7MRMHgZzbH6Vku9iJc", # My Mix 7 + "RDTMAK5uy_n_5IN6hzAOwdCnM8D8rzrs3vDl12UcZpA", # Discover Mix + "RDTMAK5uy_lr0LWzGrq6FU9GIxWvFHTRPQD2LHMqlFA", # New Release Mix + "RDTMAK5uy_nilrsVWxrKskY0ZUpVZ3zpB0u4LwWTVJ4", # Replay Mix + "RDTMAK5uy_mZtXeU08kxXJOUhL0ETdAuZTh1z7aAFAo", # Archive Mix +) +YTM_PREMIUM_CHECK_TRACK_ID = "dQw4w9WgXcQ" + +SUPPORTED_FEATURES = ( + ProviderFeature.LIBRARY_ARTISTS, + ProviderFeature.LIBRARY_ALBUMS, + ProviderFeature.LIBRARY_TRACKS, + ProviderFeature.LIBRARY_PLAYLISTS, + ProviderFeature.BROWSE, + ProviderFeature.SEARCH, + ProviderFeature.ARTIST_ALBUMS, + ProviderFeature.ARTIST_TOPTRACKS, + ProviderFeature.SIMILAR_TRACKS, +) + + +# TODO: fix disabled tests +# ruff: noqa: PLW2901, RET504 + + +async def setup( + mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig +) -> ProviderInstanceType: + """Initialize provider(instance) with given configuration.""" + return YoutubeMusicProvider(mass, manifest, config) + + +async def get_config_entries( + mass: MusicAssistant, + instance_id: str | None = None, # noqa: ARG001 + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, +) -> tuple[ConfigEntry, ...]: + """ + Return Config entries to setup this provider. + + instance_id: id of an existing provider instance (None if new instance setup). + action: [optional] action key called from config entries UI. + values: the (intermediate) raw values for config entries sent with the action. + """ + if action == CONF_ACTION_AUTH: + async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: + token = await login_oauth(auth_helper) + values[CONF_AUTH_TOKEN] = token["access_token"] + values[CONF_REFRESH_TOKEN] = token["refresh_token"] + values[CONF_EXPIRY_TIME] = token["expires_in"] + values[CONF_TOKEN_TYPE] = token["token_type"] + # return the collected config entries + return ( + ConfigEntry( + key=CONF_AUTH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Authentication token for Youtube Music", + description="You need to link Music Assistant to your Youtube Music account. " + "Please ignore the code on the page the next page and click 'Next'.", + action=CONF_ACTION_AUTH, + action_label="Authenticate on Youtube Music", + value=values.get(CONF_AUTH_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_REFRESH_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label=CONF_REFRESH_TOKEN, + hidden=True, + value=values.get(CONF_REFRESH_TOKEN) if values else None, + ), + ConfigEntry( + key=CONF_EXPIRY_TIME, + type=ConfigEntryType.INTEGER, + label="Expiry time of auth token for Youtube Music", + hidden=True, + value=values.get(CONF_EXPIRY_TIME) if values else None, + ), + ConfigEntry( + key=CONF_TOKEN_TYPE, + type=ConfigEntryType.STRING, + label="The token type required to create headers", + hidden=True, + value=values.get(CONF_TOKEN_TYPE) if values else None, + ), + ) + + +class YoutubeMusicProvider(MusicProvider): + """Provider for Youtube Music.""" + + _headers = None + _context = None + _cookies = None + _cipher = None + + async def handle_async_init(self) -> None: + """Set up the YTMusic provider.""" + logging.getLogger("yt_dlp").setLevel(self.logger.level + 10) + if not self.config.get_value(CONF_AUTH_TOKEN): + msg = "Invalid login credentials" + raise LoginFailed(msg) + self._initialize_headers() + self._initialize_context() + self._cookies = {"CONSENT": "YES+1"} + # get default language (that is supported by YTM) + mass_locale = self.mass.metadata.locale + for lang_code in SUPPORTED_LANGUAGES: + if lang_code in (mass_locale, mass_locale.split("_")[0]): + self.language = lang_code + break + else: + self.language = "en" + if not await self._user_has_ytm_premium(): + raise LoginFailed("User does not have Youtube Music Premium") + + @property + def supported_features(self) -> tuple[ProviderFeature, ...]: + """Return the features supported by this Provider.""" + return SUPPORTED_FEATURES + + async def search( + self, search_query: str, media_types=list[MediaType], limit: int = 5 + ) -> SearchResults: + """Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + parsed_results = SearchResults() + ytm_filter = None + if len(media_types) == 1: + # YTM does not support multiple searchtypes, falls back to all if no type given + if media_types[0] == MediaType.ARTIST: + ytm_filter = "artists" + if media_types[0] == MediaType.ALBUM: + ytm_filter = "albums" + if media_types[0] == MediaType.TRACK: + ytm_filter = "songs" + if media_types[0] == MediaType.PLAYLIST: + ytm_filter = "playlists" + if media_types[0] == MediaType.RADIO: + # bit of an edge case but still good to handle + return parsed_results + results = await search( + query=search_query, ytm_filter=ytm_filter, limit=limit, language=self.language + ) + parsed_results = SearchResults() + for result in results: + try: + if result["resultType"] == "artist" and MediaType.ARTIST in media_types: + parsed_results.artists.append(self._parse_artist(result)) + elif result["resultType"] == "album" and MediaType.ALBUM in media_types: + parsed_results.albums.append(self._parse_album(result)) + elif result["resultType"] == "playlist" and MediaType.PLAYLIST in media_types: + parsed_results.playlists.append(self._parse_playlist(result)) + elif ( + result["resultType"] in ("song", "video") + and MediaType.TRACK in media_types + and (track := self._parse_track(result)) + ): + parsed_results.tracks.append(track) + except InvalidDataError: + pass # ignore invalid item + return parsed_results + + async def get_library_artists(self) -> AsyncGenerator[Artist, None]: + """Retrieve all library artists from Youtube Music.""" + await self._check_oauth_token() + artists_obj = await get_library_artists(headers=self._headers, language=self.language) + for artist in artists_obj: + yield self._parse_artist(artist) + + async def get_library_albums(self) -> AsyncGenerator[Album, None]: + """Retrieve all library albums from Youtube Music.""" + await self._check_oauth_token() + albums_obj = await get_library_albums(headers=self._headers, language=self.language) + for album in albums_obj: + yield self._parse_album(album, album["browseId"]) + + async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: + """Retrieve all library playlists from the provider.""" + await self._check_oauth_token() + playlists_obj = await get_library_playlists(headers=self._headers, language=self.language) + for playlist in playlists_obj: + yield self._parse_playlist(playlist) + + async def get_library_tracks(self) -> AsyncGenerator[Track, None]: + """Retrieve library tracks from Youtube Music.""" + await self._check_oauth_token() + tracks_obj = await get_library_tracks(headers=self._headers, language=self.language) + for track in tracks_obj: + # Library tracks sometimes do not have a valid artist id + # In that case, call the API for track details based on track id + try: + yield self._parse_track(track) + except InvalidDataError: + track = await self.get_track(track["videoId"]) + yield track + + async def get_album(self, prov_album_id) -> Album: + """Get full album details by id.""" + await self._check_oauth_token() + if album_obj := await get_album(prov_album_id=prov_album_id, language=self.language): + return self._parse_album(album_obj=album_obj, album_id=prov_album_id) + msg = f"Item {prov_album_id} not found" + raise MediaNotFoundError(msg) + + async def get_album_tracks(self, prov_album_id: str) -> list[Track]: + """Get album tracks for given album id.""" + await self._check_oauth_token() + album_obj = await get_album(prov_album_id=prov_album_id, language=self.language) + if not album_obj.get("tracks"): + return [] + tracks = [] + for track_obj in album_obj["tracks"]: + try: + track = self._parse_track(track_obj=track_obj) + except InvalidDataError: + continue + tracks.append(track) + return tracks + + async def get_artist(self, prov_artist_id) -> Artist: + """Get full artist details by id.""" + await self._check_oauth_token() + if artist_obj := await get_artist( + prov_artist_id=prov_artist_id, headers=self._headers, language=self.language + ): + return self._parse_artist(artist_obj=artist_obj) + msg = f"Item {prov_artist_id} not found" + raise MediaNotFoundError(msg) + + async def get_track(self, prov_track_id) -> Track: + """Get full track details by id.""" + await self._check_oauth_token() + if track_obj := await get_track( + prov_track_id=prov_track_id, + headers=self._headers, + language=self.language, + ): + return self._parse_track(track_obj) + msg = f"Item {prov_track_id} not found" + raise MediaNotFoundError(msg) + + async def get_playlist(self, prov_playlist_id) -> Playlist: + """Get full playlist details by id.""" + await self._check_oauth_token() + # Grab the playlist id from the full url in case of personal playlists + if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: + prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] + if playlist_obj := await get_playlist( + prov_playlist_id=prov_playlist_id, headers=self._headers, language=self.language + ): + return self._parse_playlist(playlist_obj) + msg = f"Item {prov_playlist_id} not found" + raise MediaNotFoundError(msg) + + async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: + """Return playlist tracks for the given provider playlist id.""" + if page > 0: + # paging not supported, we always return the whole list at once + return [] + await self._check_oauth_token() + # Grab the playlist id from the full url in case of personal playlists + if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: + prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] + # Add a try to prevent MA from stopping syncing whenever we fail a single playlist + try: + playlist_obj = await get_playlist( + prov_playlist_id=prov_playlist_id, headers=self._headers + ) + except KeyError as ke: + self.logger.warning("Could not load playlist: %s: %s", prov_playlist_id, ke) + return [] + if "tracks" not in playlist_obj: + return [] + result = [] + # TODO: figure out how to handle paging in YTM + for index, track_obj in enumerate(playlist_obj["tracks"], 1): + if track_obj["isAvailable"]: + # Playlist tracks sometimes do not have a valid artist id + # In that case, call the API for track details based on track id + try: + if track := self._parse_track(track_obj): + track.position = index + result.append(track) + except InvalidDataError: + if track := await self.get_track(track_obj["videoId"]): + track.position = index + result.append(track) + # YTM doesn't seem to support paging so we ignore offset and limit + return result + + async def get_artist_albums(self, prov_artist_id) -> list[Album]: + """Get a list of albums for the given artist.""" + await self._check_oauth_token() + artist_obj = await get_artist(prov_artist_id=prov_artist_id, headers=self._headers) + if "albums" in artist_obj and "results" in artist_obj["albums"]: + albums = [] + for album_obj in artist_obj["albums"]["results"]: + if "artists" not in album_obj: + album_obj["artists"] = [ + {"id": artist_obj["channelId"], "name": artist_obj["name"]} + ] + albums.append(self._parse_album(album_obj, album_obj["browseId"])) + return albums + return [] + + async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: + """Get a list of 25 most popular tracks for the given artist.""" + await self._check_oauth_token() + artist_obj = await get_artist(prov_artist_id=prov_artist_id, headers=self._headers) + if artist_obj.get("songs") and artist_obj["songs"].get("browseId"): + prov_playlist_id = artist_obj["songs"]["browseId"] + playlist_tracks = await self.get_playlist_tracks(prov_playlist_id) + return playlist_tracks[:25] + return [] + + async def library_add(self, item: MediaItemType) -> bool: + """Add an item to the library.""" + await self._check_oauth_token() + result = False + if item.media_type == MediaType.ARTIST: + result = await library_add_remove_artist( + headers=self._headers, prov_artist_id=item.item_id, add=True + ) + elif item.media_type == MediaType.ALBUM: + result = await library_add_remove_album( + headers=self._headers, prov_item_id=item.item_id, add=True + ) + elif item.media_type == MediaType.PLAYLIST: + result = await library_add_remove_playlist( + headers=self._headers, prov_item_id=item.item_id, add=True + ) + elif item.media_type == MediaType.TRACK: + raise NotImplementedError + return result + + async def library_remove(self, prov_item_id, media_type: MediaType): + """Remove an item from the library.""" + await self._check_oauth_token() + result = False + try: + if media_type == MediaType.ARTIST: + result = await library_add_remove_artist( + headers=self._headers, prov_artist_id=prov_item_id, add=False + ) + elif media_type == MediaType.ALBUM: + result = await library_add_remove_album( + headers=self._headers, prov_item_id=prov_item_id, add=False + ) + elif media_type == MediaType.PLAYLIST: + result = await library_add_remove_playlist( + headers=self._headers, prov_item_id=prov_item_id, add=False + ) + elif media_type == MediaType.TRACK: + raise NotImplementedError + except YTMusicServerError as err: + # YTM raises if trying to remove an item that is not in the library + raise NotImplementedError(err) from err + return result + + async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: + """Add track(s) to playlist.""" + await self._check_oauth_token() + # Grab the playlist id from the full url in case of personal playlists + if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: + prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] + return await add_remove_playlist_tracks( + headers=self._headers, + prov_playlist_id=prov_playlist_id, + prov_track_ids=prov_track_ids, + add=True, + ) + + async def remove_playlist_tracks( + self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] + ) -> None: + """Remove track(s) from playlist.""" + await self._check_oauth_token() + # Grab the playlist id from the full url in case of personal playlists + if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: + prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] + playlist_obj = await get_playlist(prov_playlist_id=prov_playlist_id, headers=self._headers) + if "tracks" not in playlist_obj: + return None + tracks_to_delete = [] + for index, track in enumerate(playlist_obj["tracks"]): + if index in positions_to_remove: + # YT needs both the videoId and the setVideoId in order to remove + # the track. Thus, we need to obtain the playlist details and + # grab the info from there. + tracks_to_delete.append( + {"videoId": track["videoId"], "setVideoId": track["setVideoId"]} + ) + + return await add_remove_playlist_tracks( + headers=self._headers, + prov_playlist_id=prov_playlist_id, + prov_track_ids=tracks_to_delete, + add=False, + ) + + async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: + """Retrieve a dynamic list of tracks based on the provided item.""" + await self._check_oauth_token() + result = [] + result = await get_song_radio_tracks( + headers=self._headers, + prov_item_id=prov_track_id, + limit=limit, + ) + if "tracks" in result: + tracks = [] + for track in result["tracks"]: + # Playlist tracks sometimes do not have a valid artist id + # In that case, call the API for track details based on track id + try: + track = self._parse_track(track) + if track: + tracks.append(track) + except InvalidDataError: + if track := await self.get_track(track["videoId"]): + tracks.append(track) + return tracks + return [] + + async def get_stream_details(self, item_id: str) -> StreamDetails: + """Return the content details for the given track when it will be streamed.""" + stream_format = await self._get_stream_format(item_id=item_id) + self.logger.debug("Found stream_format: %s for song %s", stream_format["format"], item_id) + stream_details = StreamDetails( + provider=self.instance_id, + item_id=item_id, + audio_format=AudioFormat( + content_type=ContentType.try_parse(stream_format["audio_ext"]), + ), + stream_type=StreamType.HTTP, + path=stream_format["url"], + ) + if ( + stream_format.get("audio_channels") + and str(stream_format.get("audio_channels")).isdigit() + ): + stream_details.audio_format.channels = int(stream_format.get("audio_channels")) + if stream_format.get("asr"): + stream_details.audio_format.sample_rate = int(stream_format.get("asr")) + return stream_details + + async def _post_data(self, endpoint: str, data: dict[str, str], **kwargs): + """Post data to the given endpoint.""" + await self._check_oauth_token() + url = f"{YTM_BASE_URL}{endpoint}" + data.update(self._context) + async with self.mass.http_session.post( + url, + headers=self._headers, + json=data, + ssl=False, + cookies=self._cookies, + ) as response: + return await response.json() + + async def _get_data(self, url: str, params: dict | None = None): + """Get data from the given URL.""" + await self._check_oauth_token() + async with self.mass.http_session.get( + url, headers=self._headers, params=params, cookies=self._cookies + ) as response: + return await response.text() + + async def _check_oauth_token(self) -> None: + """Verify the OAuth token is valid and refresh if needed.""" + if self.config.get_value(CONF_EXPIRY_TIME) < time(): + token = await refresh_oauth_token( + self.mass.http_session, self.config.get_value(CONF_REFRESH_TOKEN) + ) + self.config.update({CONF_AUTH_TOKEN: token["access_token"]}) + self.config.update({CONF_EXPIRY_TIME: time() + token["expires_in"]}) + self.config.update({CONF_TOKEN_TYPE: token["token_type"]}) + self._initialize_headers() + + def _initialize_headers(self) -> dict[str, str]: + """Return headers to include in the requests.""" + auth = f"{self.config.get_value(CONF_TOKEN_TYPE)} {self.config.get_value(CONF_AUTH_TOKEN)}" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", # noqa: E501 + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.5", + "Content-Type": "application/json", + "X-Goog-AuthUser": "0", + "x-origin": YTM_DOMAIN, + "X-Goog-Request-Time": str(int(time())), + "Authorization": auth, + } + self._headers = headers + + def _initialize_context(self) -> dict[str, str]: + """Return a dict to use as a context in requests.""" + self._context = { + "context": { + "client": {"clientName": "WEB_REMIX", "clientVersion": "0.1"}, + "user": {}, + } + } + + def _parse_album(self, album_obj: dict, album_id: str | None = None) -> Album: + """Parse a YT Album response to an Album model object.""" + album_id = album_id or album_obj.get("id") or album_obj.get("browseId") + if "title" in album_obj: + name = album_obj["title"] + elif "name" in album_obj: + name = album_obj["name"] + album = Album( + item_id=album_id, + name=name, + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=str(album_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f"{YTM_DOMAIN}/playlist?list={album_id}", + ) + }, + ) + if album_obj.get("year") and album_obj["year"].isdigit(): + album.year = album_obj["year"] + if "thumbnails" in album_obj: + album.metadata.images = self._parse_thumbnails(album_obj["thumbnails"]) + if description := album_obj.get("description"): + album.metadata.description = unquote(description) + if "isExplicit" in album_obj: + album.metadata.explicit = album_obj["isExplicit"] + if "artists" in album_obj: + album.artists = [ + self._get_artist_item_mapping(artist) + for artist in album_obj["artists"] + if artist.get("id") + or artist.get("channelId") + or artist.get("name") == "Various Artists" + ] + if "type" in album_obj: + if album_obj["type"] == "Single": + album_type = AlbumType.SINGLE + elif album_obj["type"] == "EP": + album_type = AlbumType.EP + elif album_obj["type"] == "Album": + album_type = AlbumType.ALBUM + else: + album_type = AlbumType.UNKNOWN + album.album_type = album_type + return album + + def _parse_artist(self, artist_obj: dict) -> Artist: + """Parse a YT Artist response to Artist model object.""" + artist_id = None + if "channelId" in artist_obj: + artist_id = artist_obj["channelId"] + elif artist_obj.get("id"): + artist_id = artist_obj["id"] + elif artist_obj["name"] == "Various Artists": + artist_id = VARIOUS_ARTISTS_YTM_ID + if not artist_id: + msg = "Artist does not have a valid ID" + raise InvalidDataError(msg) + artist = Artist( + item_id=artist_id, + name=artist_obj["name"], + provider=self.domain, + provider_mappings={ + ProviderMapping( + item_id=str(artist_id), + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f"{YTM_DOMAIN}/channel/{artist_id}", + ) + }, + ) + if "description" in artist_obj: + artist.metadata.description = artist_obj["description"] + if artist_obj.get("thumbnails"): + artist.metadata.images = self._parse_thumbnails(artist_obj["thumbnails"]) + return artist + + def _parse_playlist(self, playlist_obj: dict) -> Playlist: + """Parse a YT Playlist response to a Playlist object.""" + playlist_id = playlist_obj["id"] + playlist_name = playlist_obj["title"] + # Playlist ID's are not unique across instances for lists like 'Likes', 'Supermix', etc. + # So suffix with the instance id to make them unique + if playlist_id in YT_PERSONAL_PLAYLISTS: + playlist_id = f"{playlist_id}{YT_PLAYLIST_ID_DELIMITER}{self.instance_id}" + playlist_name = f"{playlist_name} ({self.name})" + playlist = Playlist( + item_id=playlist_id, + provider=self.domain, + name=playlist_name, + provider_mappings={ + ProviderMapping( + item_id=playlist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=f"{YTM_DOMAIN}/playlist?list={playlist_id}", + ) + }, + ) + if "description" in playlist_obj: + playlist.metadata.description = playlist_obj["description"] + if playlist_obj.get("thumbnails"): + playlist.metadata.images = self._parse_thumbnails(playlist_obj["thumbnails"]) + is_editable = False + if playlist_obj.get("privacy") and playlist_obj.get("privacy") == "PRIVATE": + is_editable = True + playlist.is_editable = is_editable + if authors := playlist_obj.get("author"): + if isinstance(authors, str): + playlist.owner = authors + elif isinstance(authors, list): + playlist.owner = authors[0]["name"] + else: + playlist.owner = authors["name"] + else: + playlist.owner = self.name + playlist.cache_checksum = playlist_obj.get("checksum") + return playlist + + def _parse_track(self, track_obj: dict) -> Track: + """Parse a YT Track response to a Track model object.""" + if not track_obj.get("videoId"): + msg = "Track is missing videoId" + raise InvalidDataError(msg) + track_id = str(track_obj["videoId"]) + track = Track( + item_id=track_id, + provider=self.domain, + name=track_obj["title"], + provider_mappings={ + ProviderMapping( + item_id=track_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + available=track_obj.get("isAvailable", True), + url=f"{YTM_DOMAIN}/watch?v={track_id}", + audio_format=AudioFormat( + content_type=ContentType.M4A, + ), + ) + }, + disc_number=0, # not supported on YTM? + track_number=track_obj.get("trackNumber", 0), + ) + + if track_obj.get("artists"): + track.artists = [ + self._get_artist_item_mapping(artist) + for artist in track_obj["artists"] + if artist.get("id") + or artist.get("channelId") + or artist.get("name") == "Various Artists" + ] + # guard that track has valid artists + if not track.artists: + msg = "Track is missing artists" + raise InvalidDataError(msg) + if track_obj.get("thumbnails"): + track.metadata.images = self._parse_thumbnails(track_obj["thumbnails"]) + if ( + track_obj.get("album") + and isinstance(track_obj.get("album"), dict) + and track_obj["album"].get("id") + ): + album = track_obj["album"] + track.album = self._get_item_mapping(MediaType.ALBUM, album["id"], album["name"]) + if "isExplicit" in track_obj: + track.metadata.explicit = track_obj["isExplicit"] + if "duration" in track_obj and str(track_obj["duration"]).isdigit(): + track.duration = int(track_obj["duration"]) + elif "duration_seconds" in track_obj and str(track_obj["duration_seconds"]).isdigit(): + track.duration = int(track_obj["duration_seconds"]) + return track + + async def _get_stream_format(self, item_id: str) -> dict[str, Any]: + """Figure out the stream URL to use and return the highest quality.""" + await self._check_oauth_token() + + def _extract_best_stream_url_format() -> dict[str, Any]: + url = f"{YTM_DOMAIN}/watch?v={item_id}" + auth = ( + f"{self.config.get_value(CONF_TOKEN_TYPE)} {self.config.get_value(CONF_AUTH_TOKEN)}" + ) + ydl_opts = { + "quiet": self.logger.level > logging.DEBUG, + # This enables the access token plugin so we can grab the best + # available quality audio stream + "username": auth, + # This enforces a player client and skips unnecessary scraping to increase speed + "extractor_args": { + "youtube": {"skip": ["translated_subs", "dash"], "player_client": ["ios"]} + }, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + try: + info = ydl.extract_info(url, download=False) + except yt_dlp.utils.DownloadError as err: + raise UnplayableMediaError(err) from err + format_selector = ydl.build_format_selector("m4a/bestaudio") + stream_format = next(format_selector({"formats": info["formats"]})) + return stream_format + + return await asyncio.to_thread(_extract_best_stream_url_format) + + def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: + return ItemMapping( + media_type=media_type, + item_id=key, + provider=self.instance_id, + name=name, + ) + + def _get_artist_item_mapping(self, artist_obj: dict) -> ItemMapping: + artist_id = artist_obj.get("id") or artist_obj.get("channelId") + if not artist_id and artist_obj["name"] == "Various Artists": + artist_id = VARIOUS_ARTISTS_YTM_ID + return self._get_item_mapping(MediaType.ARTIST, artist_id, artist_obj.get("name")) + + async def _user_has_ytm_premium(self) -> bool: + """Check if the user has Youtube Music Premium.""" + stream_format = await self._get_stream_format(YTM_PREMIUM_CHECK_TRACK_ID) + # Only premium users can stream the HQ stream of this song + return stream_format["format_id"] == "141" + + def _parse_thumbnails(self, thumbnails_obj: dict) -> list[MediaItemImage]: + """Parse and YTM thumbnails to MediaItemImage.""" + result: list[MediaItemImage] = [] + processed_images = set() + for img in sorted(thumbnails_obj, key=lambda w: w.get("width", 0), reverse=True): + url: str = img["url"] + url_base = url.split("=w")[0] + width: int = img["width"] + height: int = img["height"] + image_ratio: float = width / height + image_type = ( + ImageType.LANDSCAPE + if "maxresdefault" in url or image_ratio > 2.0 + else ImageType.THUMB + ) + if "=w" not in url and width < 500: + continue + # if the size is in the url, we can actually request a higher thumb + if "=w" in url and width < 600: + url = f"{url_base}=w600-h600-p" + image_type = ImageType.THUMB + if (url_base, image_type) in processed_images: + continue + processed_images.add((url_base, image_type)) + result.append( + MediaItemImage( + type=image_type, + path=url, + provider=self.lookup_key, + remotely_accessible=True, + ) + ) + return result diff --git a/music_assistant/providers/ytmusic/helpers.py b/music_assistant/providers/ytmusic/helpers.py new file mode 100644 index 00000000..f41ac68c --- /dev/null +++ b/music_assistant/providers/ytmusic/helpers.py @@ -0,0 +1,370 @@ +"""Helper module for parsing the Youtube Music API. + +This helpers file is an async wrapper around the excellent ytmusicapi package. +While the ytmusicapi package does an excellent job at parsing the Youtube Music results, +it is unfortunately not async, which is required for Music Assistant to run smoothly. +This also nicely separates the parsing logic from the Youtube Music provider logic. +""" + +import asyncio +from time import time + +import ytmusicapi +from aiohttp import ClientSession +from ytmusicapi.constants import ( + OAUTH_CLIENT_ID, + OAUTH_CLIENT_SECRET, + OAUTH_CODE_URL, + OAUTH_SCOPE, + OAUTH_TOKEN_URL, + OAUTH_USER_AGENT, +) + +from music_assistant.helpers.auth import AuthenticationHelper + + +async def get_artist( + prov_artist_id: str, headers: dict[str, str], language: str = "en" +) -> dict[str, str]: + """Async wrapper around the ytmusicapi get_artist function.""" + + def _get_artist(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + try: + artist = ytm.get_artist(channelId=prov_artist_id) + # ChannelId can sometimes be different and original ID is not part of the response + artist["channelId"] = prov_artist_id + except KeyError: + try: + user = ytm.get_user(channelId=prov_artist_id) + artist = {"channelId": prov_artist_id, "name": user["name"]} + except KeyError: + artist = {"channelId": prov_artist_id, "name": "Unknown"} + return artist + + return await asyncio.to_thread(_get_artist) + + +async def get_album(prov_album_id: str, language: str = "en") -> dict[str, str]: + """Async wrapper around the ytmusicapi get_album function.""" + + def _get_album(): + ytm = ytmusicapi.YTMusic(language=language) + return ytm.get_album(browseId=prov_album_id) + + return await asyncio.to_thread(_get_album) + + +async def get_playlist( + prov_playlist_id: str, headers: dict[str, str], language: str = "en" +) -> dict[str, str]: + """Async wrapper around the ytmusicapi get_playlist function.""" + + def _get_playlist(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + playlist = ytm.get_playlist(playlistId=prov_playlist_id, limit=None) + playlist["checksum"] = get_playlist_checksum(playlist) + # Fix missing playlist id in some edge cases + playlist["id"] = prov_playlist_id if not playlist.get("id") else playlist["id"] + return playlist + + return await asyncio.to_thread(_get_playlist) + + +async def get_track( + prov_track_id: str, headers: dict[str, str], language: str = "en" +) -> dict[str, str] | None: + """Async wrapper around the ytmusicapi get_playlist function.""" + + def _get_song(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + track_obj = ytm.get_song(videoId=prov_track_id) + track = {} + if "videoDetails" not in track_obj: + # video that no longer exists + return None + track["videoId"] = track_obj["videoDetails"]["videoId"] + track["title"] = track_obj["videoDetails"]["title"] + track["artists"] = [ + { + "channelId": track_obj["videoDetails"]["channelId"], + "name": track_obj["videoDetails"]["author"], + } + ] + track["duration"] = track_obj["videoDetails"]["lengthSeconds"] + track["thumbnails"] = track_obj["microformat"]["microformatDataRenderer"]["thumbnail"][ + "thumbnails" + ] + track["isAvailable"] = track_obj["playabilityStatus"]["status"] == "OK" + return track + + return await asyncio.to_thread(_get_song) + + +async def get_library_artists(headers: dict[str, str], language: str = "en") -> dict[str, str]: + """Async wrapper around the ytmusicapi get_library_artists function.""" + + def _get_library_artists(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + artists = ytm.get_library_subscriptions(limit=9999) + # Sync properties with uniformal artist object + for artist in artists: + artist["id"] = artist["browseId"] + artist["name"] = artist["artist"] + del artist["browseId"] + del artist["artist"] + return artists + + return await asyncio.to_thread(_get_library_artists) + + +async def get_library_albums(headers: dict[str, str], language: str = "en") -> dict[str, str]: + """Async wrapper around the ytmusicapi get_library_albums function.""" + + def _get_library_albums(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + return ytm.get_library_albums(limit=9999) + + return await asyncio.to_thread(_get_library_albums) + + +async def get_library_playlists(headers: dict[str, str], language: str = "en") -> dict[str, str]: + """Async wrapper around the ytmusicapi get_library_playlists function.""" + + def _get_library_playlists(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + playlists = ytm.get_library_playlists(limit=9999) + # Sync properties with uniformal playlist object + for playlist in playlists: + playlist["id"] = playlist["playlistId"] + del playlist["playlistId"] + playlist["checksum"] = get_playlist_checksum(playlist) + return playlists + + return await asyncio.to_thread(_get_library_playlists) + + +async def get_library_tracks(headers: dict[str, str], language: str = "en") -> dict[str, str]: + """Async wrapper around the ytmusicapi get_library_tracks function.""" + + def _get_library_tracks(): + ytm = ytmusicapi.YTMusic(auth=headers, language=language) + return ytm.get_library_songs(limit=9999) + + return await asyncio.to_thread(_get_library_tracks) + + +async def library_add_remove_artist( + headers: dict[str, str], prov_artist_id: str, add: bool = True +) -> bool: + """Add or remove an artist to the user's library.""" + + def _library_add_remove_artist(): + ytm = ytmusicapi.YTMusic(auth=headers) + if add: + return "actions" in ytm.subscribe_artists(channelIds=[prov_artist_id]) + if not add: + return "actions" in ytm.unsubscribe_artists(channelIds=[prov_artist_id]) + return None + + return await asyncio.to_thread(_library_add_remove_artist) + + +async def library_add_remove_album( + headers: dict[str, str], prov_item_id: str, add: bool = True +) -> bool: + """Add or remove an album or playlist to the user's library.""" + album = await get_album(prov_album_id=prov_item_id) + + def _library_add_remove_album(): + ytm = ytmusicapi.YTMusic(auth=headers) + playlist_id = album["audioPlaylistId"] + if add: + return ytm.rate_playlist(playlist_id, "LIKE") + if not add: + return ytm.rate_playlist(playlist_id, "INDIFFERENT") + return None + + return await asyncio.to_thread(_library_add_remove_album) + + +async def library_add_remove_playlist( + headers: dict[str, str], prov_item_id: str, add: bool = True +) -> bool: + """Add or remove an album or playlist to the user's library.""" + + def _library_add_remove_playlist(): + ytm = ytmusicapi.YTMusic(auth=headers) + if add: + return "actions" in ytm.rate_playlist(prov_item_id, "LIKE") + if not add: + return "actions" in ytm.rate_playlist(prov_item_id, "INDIFFERENT") + return None + + return await asyncio.to_thread(_library_add_remove_playlist) + + +async def add_remove_playlist_tracks( + headers: dict[str, str], prov_playlist_id: str, prov_track_ids: list[str], add: bool +) -> bool: + """Async wrapper around adding/removing tracks to a playlist.""" + + def _add_playlist_tracks(): + ytm = ytmusicapi.YTMusic(auth=headers) + if add: + return ytm.add_playlist_items(playlistId=prov_playlist_id, videoIds=prov_track_ids) + if not add: + return ytm.remove_playlist_items(playlistId=prov_playlist_id, videos=prov_track_ids) + return None + + return await asyncio.to_thread(_add_playlist_tracks) + + +async def get_song_radio_tracks( + headers: dict[str, str], prov_item_id: str, limit=25 +) -> dict[str, str]: + """Async wrapper around the ytmusicapi radio function.""" + + def _get_song_radio_tracks(): + ytm = ytmusicapi.YTMusic(auth=headers) + playlist_id = f"RDAMVM{prov_item_id}" + result = ytm.get_watch_playlist( + videoId=prov_item_id, playlistId=playlist_id, limit=limit, radio=True + ) + # Replace inconsistensies for easier parsing + for track in result["tracks"]: + if track.get("thumbnail"): + track["thumbnails"] = track["thumbnail"] + del track["thumbnail"] + if track.get("length"): + track["duration"] = get_sec(track["length"]) + return result + + return await asyncio.to_thread(_get_song_radio_tracks) + + +async def search( + query: str, ytm_filter: str | None = None, limit: int = 20, language: str = "en" +) -> list[dict]: + """Async wrapper around the ytmusicapi search function.""" + + def _search(): + ytm = ytmusicapi.YTMusic(language=language) + results = ytm.search(query=query, filter=ytm_filter, limit=limit) + # Sync result properties with uniformal objects + for result in results: + if result["resultType"] == "artist": + if "artists" in result and len(result["artists"]) > 0: + result["id"] = result["artists"][0]["id"] + result["name"] = result["artists"][0]["name"] + del result["artists"] + else: + result["id"] = result["browseId"] + result["name"] = result["artist"] + del result["browseId"] + del result["artist"] + elif result["resultType"] == "playlist": + if "playlistId" in result: + result["id"] = result["playlistId"] + del result["playlistId"] + elif "browseId" in result: + result["id"] = result["browseId"] + del result["browseId"] + return results[:limit] + + return await asyncio.to_thread(_search) + + +def get_playlist_checksum(playlist_obj: dict) -> str: + """Try to calculate a checksum so we can detect changes in a playlist.""" + for key in ("duration_seconds", "trackCount", "count"): + if key in playlist_obj: + return playlist_obj[key] + return str(int(time())) + + +def is_brand_account(username: str) -> bool: + """Check if the provided username is a brand-account.""" + return len(username) == 21 and username.isdigit() + + +def get_sec(time_str): + """Get seconds from time.""" + parts = time_str.split(":") + if len(parts) == 3: + return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) + if len(parts) == 2: + return int(parts[0]) * 60 + int(parts[1]) + return 0 + + +async def login_oauth(auth_helper: AuthenticationHelper): + """Use device login to get a token.""" + http_session = auth_helper.mass.http_session + code = await get_oauth_code(http_session) + return await visit_oauth_auth_url(auth_helper, code) + + +def _get_data_and_headers(data: dict): + """Prepare headers for OAuth requests.""" + data.update({"client_id": OAUTH_CLIENT_ID}) + headers = {"User-Agent": OAUTH_USER_AGENT} + return data, headers + + +async def get_oauth_code(session: ClientSession): + """Get the OAuth code from the server.""" + data, headers = _get_data_and_headers({"scope": OAUTH_SCOPE}) + async with session.post(OAUTH_CODE_URL, json=data, headers=headers) as code_response: + return await code_response.json() + + +async def visit_oauth_auth_url(auth_helper: AuthenticationHelper, code: dict[str, str]): + """Redirect the user to the OAuth login page and wait for the token.""" + auth_url = f"{code['verification_url']}?user_code={code['user_code']}" + auth_helper.send_url(auth_url=auth_url) + device_code = code["device_code"] + expiry = code["expires_in"] + interval = code["interval"] + while expiry > 0: + token = await get_oauth_token_from_code(auth_helper.mass.http_session, device_code) + if token.get("access_token"): + return token + await asyncio.sleep(interval) + expiry -= interval + msg = "You took too long to log in" + raise TimeoutError(msg) + + +async def get_oauth_token_from_code(session: ClientSession, device_code: str): + """Check if the OAuth token is ready yet.""" + data, headers = _get_data_and_headers( + data={ + "client_secret": OAUTH_CLIENT_SECRET, + "grant_type": "http://oauth.net/grant_type/device/1.0", + "code": device_code, + } + ) + async with session.post( + OAUTH_TOKEN_URL, + json=data, + headers=headers, + ) as token_response: + return await token_response.json() + + +async def refresh_oauth_token(session: ClientSession, refresh_token: str): + """Refresh an expired OAuth token.""" + data, headers = _get_data_and_headers( + { + "client_secret": OAUTH_CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": refresh_token, + } + ) + async with session.post( + OAUTH_TOKEN_URL, + json=data, + headers=headers, + ) as response: + return await response.json() diff --git a/music_assistant/providers/ytmusic/icon.svg b/music_assistant/providers/ytmusic/icon.svg new file mode 100644 index 00000000..22ba913e --- /dev/null +++ b/music_assistant/providers/ytmusic/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/music_assistant/providers/ytmusic/manifest.json b/music_assistant/providers/ytmusic/manifest.json new file mode 100644 index 00000000..30d02f38 --- /dev/null +++ b/music_assistant/providers/ytmusic/manifest.json @@ -0,0 +1,10 @@ +{ + "type": "music", + "domain": "ytmusic", + "name": "YouTube Music", + "description": "Support for the YouTube Music streaming provider in Music Assistant.", + "codeowners": ["@MarvinSchenkel"], + "requirements": ["ytmusicapi==1.8.1", "yt-dlp-youtube-accesstoken==0.1.1", "yt-dlp==2024.10.7"], + "documentation": "https://music-assistant.io/music-providers/youtube-music/", + "multi_instance": true +} diff --git a/music_assistant/server/__init__.py b/music_assistant/server/__init__.py deleted file mode 100644 index 7fe0caca..00000000 --- a/music_assistant/server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Music Assistant: The music library manager in python.""" - -from .server import MusicAssistant # noqa: F401 diff --git a/music_assistant/server/controllers/__init__.py b/music_assistant/server/controllers/__init__.py deleted file mode 100644 index 49fe05d5..00000000 --- a/music_assistant/server/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package with controllers.""" diff --git a/music_assistant/server/controllers/cache.py b/music_assistant/server/controllers/cache.py deleted file mode 100644 index 5e29edd7..00000000 --- a/music_assistant/server/controllers/cache.py +++ /dev/null @@ -1,407 +0,0 @@ -"""Provides a simple stateless caching system.""" - -from __future__ import annotations - -import asyncio -import functools -import logging -import os -import time -from collections import OrderedDict -from collections.abc import Callable, Iterator, MutableMapping -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar - -from music_assistant.common.helpers.json import json_dumps, json_loads -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.constants import DB_TABLE_CACHE, DB_TABLE_SETTINGS, MASS_LOGGER_NAME -from music_assistant.server.helpers.database import DatabaseConnection -from music_assistant.server.models.core_controller import CoreController - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.cache") -CONF_CLEAR_CACHE = "clear_cache" -DB_SCHEMA_VERSION = 5 - - -class CacheController(CoreController): - """Basic cache controller using both memory and database.""" - - domain: str = "cache" - - def __init__(self, *args, **kwargs) -> None: - """Initialize core controller.""" - super().__init__(*args, **kwargs) - self.database: DatabaseConnection | None = None - self._mem_cache = MemoryCache(500) - self.manifest.name = "Cache controller" - self.manifest.description = ( - "Music Assistant's core controller for caching data throughout the application." - ) - self.manifest.icon = "memory" - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - if action == CONF_CLEAR_CACHE: - await self.clear() - return ( - ConfigEntry( - key=CONF_CLEAR_CACHE, - type=ConfigEntryType.LABEL, - label="The cache has been cleared", - ), - ) - return ( - ConfigEntry( - key=CONF_CLEAR_CACHE, - type=ConfigEntryType.ACTION, - label="Clear cache", - description="Reset/clear all items in the cache. ", - ), - ) - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of cache module.""" - self.logger.info("Initializing cache controller...") - await self._setup_database() - self.__schedule_cleanup_task() - - async def close(self) -> None: - """Cleanup on exit.""" - await self.database.close() - - async def get( - self, - key: str, - checksum: str | None = None, - default=None, - category: int = 0, - base_key: str = "", - ) -> Any: - """Get object from cache and return the results. - - cache_key: the (unique) name of the cache object as reference - checksum: optional argument to check if the checksum in the - cacheobject matches the checksum provided - category: optional category to group cache objects - base_key: optional base key to group cache objects - """ - if not key: - return None - cur_time = int(time.time()) - if checksum is not None and not isinstance(checksum, str): - checksum = str(checksum) - - # try memory cache first - memory_key = f"{category}/{base_key}/{key}" - cache_data = self._mem_cache.get(memory_key) - if cache_data and (not checksum or cache_data[1] == checksum) and cache_data[2] >= cur_time: - return cache_data[0] - # fall back to db cache - if ( - db_row := await self.database.get_row( - DB_TABLE_CACHE, {"category": category, "base_key": base_key, "sub_key": key} - ) - ) and (not checksum or db_row["checksum"] == checksum and db_row["expires"] >= cur_time): - try: - data = await asyncio.to_thread(json_loads, db_row["data"]) - except Exception as exc: - LOGGER.error( - "Error parsing cache data for %s: %s", - memory_key, - str(exc), - exc_info=exc if self.logger.isEnabledFor(10) else None, - ) - else: - # also store in memory cache for faster access - self._mem_cache[memory_key] = ( - data, - db_row["checksum"], - db_row["expires"], - ) - return data - return default - - async def set( - self, key, data, checksum="", expiration=(86400 * 7), category: int = 0, base_key: str = "" - ) -> None: - """Set data in cache.""" - if not key: - return - if checksum is not None and not isinstance(checksum, str): - checksum = str(checksum) - expires = int(time.time() + expiration) - memory_key = f"{category}/{base_key}/{key}" - self._mem_cache[memory_key] = (data, checksum, expires) - if (expires - time.time()) < 3600 * 12: - # do not cache items in db with short expiration - return - data = await asyncio.to_thread(json_dumps, data) - await self.database.insert_or_replace( - DB_TABLE_CACHE, - { - "category": category, - "base_key": base_key, - "sub_key": key, - "expires": expires, - "checksum": checksum, - "data": data, - }, - ) - - async def delete( - self, key: str | None, category: int | None = None, base_key: str | None = None - ) -> None: - """Delete data from cache.""" - match: dict[str, str | int] = {} - if key is not None: - match["sub_key"] = key - if category is not None: - match["category"] = category - if base_key is not None: - match["base_key"] = base_key - if key is not None and category is not None and base_key is not None: - self._mem_cache.pop(f"{category}/{base_key}/{key}", None) - else: - self._mem_cache.clear() - await self.database.delete(DB_TABLE_CACHE, match) - - async def clear( - self, - key_filter: str | None = None, - category: int | None = None, - base_key_filter: str | None = None, - ) -> None: - """Clear all/partial items from cache.""" - self._mem_cache.clear() - self.logger.info("Clearing database...") - query_parts: list[str] = [] - if category is not None: - query_parts.append(f"category = {category}") - if base_key_filter is not None: - query_parts.append(f"base_key LIKE '%{base_key_filter}%'") - if key_filter is not None: - query_parts.append(f"sub_key LIKE '%{key_filter}%'") - query = "WHERE " + " AND ".join(query_parts) if query_parts else None - await self.database.delete(DB_TABLE_CACHE, query=query) - self.logger.info("Clearing database DONE") - - async def auto_cleanup(self) -> None: - """Run scheduled auto cleanup task.""" - self.logger.debug("Running automatic cleanup...") - # simply reset the memory cache - self._mem_cache.clear() - cur_timestamp = int(time.time()) - cleaned_records = 0 - for db_row in await self.database.get_rows(DB_TABLE_CACHE): - # clean up db cache object only if expired - if db_row["expires"] < cur_timestamp: - await self.database.delete(DB_TABLE_CACHE, {"id": db_row["id"]}) - cleaned_records += 1 - await asyncio.sleep(0) # yield to eventloop - self.logger.debug("Automatic cleanup finished (cleaned up %s records)", cleaned_records) - - async def _setup_database(self) -> None: - """Initialize database.""" - db_path = os.path.join(self.mass.storage_path, "cache.db") - self.database = DatabaseConnection(db_path) - await self.database.setup() - - # always create db tables if they don't exist to prevent errors trying to access them later - await self.__create_database_tables() - try: - if db_row := await self.database.get_row(DB_TABLE_SETTINGS, {"key": "version"}): - prev_version = int(db_row["value"]) - else: - prev_version = 0 - except (KeyError, ValueError): - prev_version = 0 - - if prev_version not in (0, DB_SCHEMA_VERSION): - LOGGER.warning( - "Performing database migration from %s to %s", - prev_version, - DB_SCHEMA_VERSION, - ) - - if prev_version < DB_SCHEMA_VERSION: - # for now just keep it simple and just recreate the table(s) - await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_CACHE}") - - # recreate missing table(s) - await self.__create_database_tables() - - # store current schema version - await self.database.insert_or_replace( - DB_TABLE_SETTINGS, - {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"}, - ) - await self.__create_database_indexes() - # compact db (vacuum) at startup - self.logger.debug("Compacting database...") - try: - await self.database.vacuum() - except Exception as err: - self.logger.warning("Database vacuum failed: %s", str(err)) - else: - self.logger.debug("Compacting database done") - - async def __create_database_tables(self) -> None: - """Create database table(s).""" - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_SETTINGS}( - key TEXT PRIMARY KEY, - value TEXT, - type TEXT - );""" - ) - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_CACHE}( - [id] INTEGER PRIMARY KEY AUTOINCREMENT, - [category] INTEGER NOT NULL DEFAULT 0, - [base_key] TEXT NOT NULL, - [sub_key] TEXT NOT NULL, - [expires] INTEGER NOT NULL, - [data] TEXT, - [checksum] TEXT NULL, - UNIQUE(category, base_key, sub_key) - )""" - ) - - await self.database.commit() - - async def __create_database_indexes(self) -> None: - """Create database indexes.""" - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_idx " - f"ON {DB_TABLE_CACHE}(category);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_base_key_idx " - f"ON {DB_TABLE_CACHE}(base_key);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_sub_key_idx " - f"ON {DB_TABLE_CACHE}(sub_key);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_base_key_idx " - f"ON {DB_TABLE_CACHE}(category,base_key);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_CACHE}_category_base_key_sub_key_idx " - f"ON {DB_TABLE_CACHE}(category,base_key,sub_key);" - ) - await self.database.commit() - - def __schedule_cleanup_task(self) -> None: - """Schedule the cleanup task.""" - self.mass.create_task(self.auto_cleanup()) - # reschedule self - self.mass.loop.call_later(3600, self.__schedule_cleanup_task) - - -Param = ParamSpec("Param") -RetType = TypeVar("RetType") - - -def use_cache( - expiration: int = 86400 * 30, - category: int = 0, -) -> Callable[[Callable[Param, RetType]], Callable[Param, RetType]]: - """Return decorator that can be used to cache a method's result.""" - - def wrapper(func: Callable[Param, RetType]) -> Callable[Param, RetType]: - @functools.wraps(func) - async def wrapped(*args: Param.args, **kwargs: Param.kwargs): - method_class = args[0] - method_class_name = method_class.__class__.__name__ - cache_base_key = f"{method_class_name}.{func.__name__}" - cache_sub_key_parts = [] - skip_cache = kwargs.pop("skip_cache", False) - cache_checksum = kwargs.pop("cache_checksum", "") - if len(args) > 1: - cache_sub_key_parts += args[1:] - for key in sorted(kwargs.keys()): - cache_sub_key_parts.append(f"{key}{kwargs[key]}") - cache_sub_key = ".".join(cache_sub_key_parts) - - cachedata = await method_class.cache.get( - cache_sub_key, checksum=cache_checksum, category=category, base_key=cache_base_key - ) - - if not skip_cache and cachedata is not None: - return cachedata - result = await func(*args, **kwargs) - asyncio.create_task( - method_class.cache.set( - cache_sub_key, - result, - expiration=expiration, - checksum=cache_checksum, - category=category, - base_key=cache_base_key, - ) - ) - return result - - return wrapped - - return wrapper - - -class MemoryCache(MutableMapping): - """Simple limited in-memory cache implementation.""" - - def __init__(self, maxlen: int) -> None: - """Initialize.""" - self._maxlen = maxlen - self.d = OrderedDict() - - @property - def maxlen(self) -> int: - """Return max length.""" - return self._maxlen - - def get(self, key: str, default: Any = None) -> Any: - """Return item or default.""" - return self.d.get(key, default) - - def pop(self, key: str, default: Any = None) -> Any: - """Pop item from collection.""" - return self.d.pop(key, default) - - def __getitem__(self, key: str) -> Any: - """Get item.""" - self.d.move_to_end(key) - return self.d[key] - - def __setitem__(self, key: str, value: Any) -> None: - """Set item.""" - if key in self.d: - self.d.move_to_end(key) - elif len(self.d) == self.maxlen: - self.d.popitem(last=False) - self.d[key] = value - - def __delitem__(self, key) -> None: - """Delete item.""" - del self.d[key] - - def __iter__(self) -> Iterator: - """Iterate items.""" - return self.d.__iter__() - - def __len__(self) -> int: - """Return length.""" - return len(self.d) - - def clear(self) -> None: - """Clear cache.""" - self.d.clear() diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py deleted file mode 100644 index d77462a6..00000000 --- a/music_assistant/server/controllers/config.py +++ /dev/null @@ -1,841 +0,0 @@ -"""Logic to handle storage of persistent (configuration) settings.""" - -from __future__ import annotations - -import base64 -import logging -import os -from contextlib import suppress -from typing import TYPE_CHECKING, Any -from uuid import uuid4 - -import aiofiles -import shortuuid -from aiofiles.os import wrap -from cryptography.fernet import Fernet, InvalidToken - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads -from music_assistant.common.models import config_entries -from music_assistant.common.models.config_entries import ( - DEFAULT_CORE_CONFIG_ENTRIES, - DEFAULT_PROVIDER_CONFIG_ENTRIES, - ConfigEntry, - ConfigValueType, - CoreConfig, - PlayerConfig, - ProviderConfig, -) -from music_assistant.common.models.enums import EventType, ProviderFeature, ProviderType -from music_assistant.common.models.errors import ( - ActionUnavailable, - InvalidDataError, - PlayerCommandFailed, - UnsupportedFeaturedException, -) -from music_assistant.constants import ( - CONF_CORE, - CONF_PLAYERS, - CONF_PROVIDERS, - CONF_SERVER_ID, - CONFIGURABLE_CORE_CONTROLLERS, - ENCRYPT_SUFFIX, -) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.util import load_provider_module - -if TYPE_CHECKING: - import asyncio - - from music_assistant.server.models.core_controller import CoreController - from music_assistant.server.server import MusicAssistant - -LOGGER = logging.getLogger(__name__) -DEFAULT_SAVE_DELAY = 5 - -BASE_KEYS = ("enabled", "name", "available", "default_name", "provider", "type") - -isfile = wrap(os.path.isfile) -remove = wrap(os.remove) -rename = wrap(os.rename) - - -class ConfigController: - """Controller that handles storage of persistent configuration settings.""" - - _fernet: Fernet | None = None - - def __init__(self, mass: MusicAssistant) -> None: - """Initialize storage controller.""" - self.mass = mass - self.initialized = False - self._data: dict[str, Any] = {} - self.filename = os.path.join(self.mass.storage_path, "settings.json") - self._timer_handle: asyncio.TimerHandle | None = None - self._value_cache: dict[str, ConfigValueType] = {} - - async def setup(self) -> None: - """Async initialize of controller.""" - await self._load() - self.initialized = True - # create default server ID if needed (also used for encrypting passwords) - self.set_default(CONF_SERVER_ID, uuid4().hex) - server_id: str = self.get(CONF_SERVER_ID) - assert server_id - fernet_key = base64.urlsafe_b64encode(server_id.encode()[:32]) - self._fernet = Fernet(fernet_key) - config_entries.ENCRYPT_CALLBACK = self.encrypt_string - config_entries.DECRYPT_CALLBACK = self.decrypt_string - LOGGER.debug("Started.") - - @property - def onboard_done(self) -> bool: - """Return True if onboarding is done.""" - return len(self._data.get(CONF_PROVIDERS, {})) > 0 - - async def close(self) -> None: - """Handle logic on server stop.""" - if not self._timer_handle: - # no point in forcing a save when there are no changes pending - return - await self._async_save() - LOGGER.debug("Stopped.") - - def get(self, key: str, default: Any = None) -> Any: - """Get value(s) for a specific key/path in persistent storage.""" - assert self.initialized, "Not yet (async) initialized" - # we support a multi level hierarchy by providing the key as path, - # with a slash (/) as splitter. Sort that out here. - parent = self._data - subkeys = key.split("/") - for index, subkey in enumerate(subkeys): - if index == (len(subkeys) - 1): - value = parent.get(subkey, default) - if value is None: - # replace None with default - return default - return value - if subkey not in parent: - # requesting subkey from a non existing parent - return default - parent = parent[subkey] - return default - - def set(self, key: str, value: Any) -> None: - """Set value(s) for a specific key/path in persistent storage.""" - assert self.initialized, "Not yet (async) initialized" - # we support a multi level hierarchy by providing the key as path, - # with a slash (/) as splitter. - parent = self._data - subkeys = key.split("/") - for index, subkey in enumerate(subkeys): - if index == (len(subkeys) - 1): - parent[subkey] = value - else: - parent.setdefault(subkey, {}) - parent = parent[subkey] - self.save() - - def set_default(self, key: str, default_value: Any) -> None: - """Set default value(s) for a specific key/path in persistent storage.""" - assert self.initialized, "Not yet (async) initialized" - cur_value = self.get(key, "__MISSING__") - if cur_value == "__MISSING__": - self.set(key, default_value) - - def remove( - self, - key: str, - ) -> None: - """Remove value(s) for a specific key/path in persistent storage.""" - assert self.initialized, "Not yet (async) initialized" - parent = self._data - subkeys = key.split("/") - for index, subkey in enumerate(subkeys): - if subkey not in parent: - return - if index == (len(subkeys) - 1): - parent.pop(subkey) - else: - parent.setdefault(subkey, {}) - parent = parent[subkey] - - self.save() - - @api_command("config/providers") - async def get_provider_configs( - self, - provider_type: ProviderType | None = None, - provider_domain: str | None = None, - include_values: bool = False, - ) -> list[ProviderConfig]: - """Return all known provider configurations, optionally filtered by ProviderType.""" - raw_values: dict[str, dict] = self.get(CONF_PROVIDERS, {}) - prov_entries = {x.domain for x in self.mass.get_provider_manifests()} - return [ - await self.get_provider_config(prov_conf["instance_id"]) - if include_values - else ProviderConfig.parse([], prov_conf) - for prov_conf in raw_values.values() - if (provider_type is None or prov_conf["type"] == provider_type) - and (provider_domain is None or prov_conf["domain"] == provider_domain) - # guard for deleted providers - and prov_conf["domain"] in prov_entries - ] - - @api_command("config/providers/get") - async def get_provider_config(self, instance_id: str) -> ProviderConfig: - """Return configuration for a single provider.""" - if raw_conf := self.get(f"{CONF_PROVIDERS}/{instance_id}", {}): - config_entries = await self.get_provider_config_entries( - raw_conf["domain"], - instance_id=instance_id, - values=raw_conf.get("values"), - ) - for prov in self.mass.get_provider_manifests(): - if prov.domain == raw_conf["domain"]: - break - else: - msg = f'Unknown provider domain: {raw_conf["domain"]}' - raise KeyError(msg) - return ProviderConfig.parse(config_entries, raw_conf) - msg = f"No config found for provider id {instance_id}" - raise KeyError(msg) - - @api_command("config/providers/get_value") - async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueType: - """Return single configentry value for a provider.""" - cache_key = f"prov_conf_value_{instance_id}.{key}" - if (cached_value := self._value_cache.get(cache_key)) is not None: - return cached_value - conf = await self.get_provider_config(instance_id) - val = ( - conf.values[key].value - if conf.values[key].value is not None - else conf.values[key].default_value - ) - # store value in cache because this method can potentially be called very often - self._value_cache[cache_key] = val - return val - - @api_command("config/providers/get_entries") - async def get_provider_config_entries( - self, - provider_domain: str, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup/configure a provider. - - provider_domain: (mandatory) domain of the provider. - instance_id: id of an existing provider instance (None for new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # lookup provider manifest and module - for prov in self.mass.get_provider_manifests(): - if prov.domain == provider_domain: - prov_mod = await load_provider_module(provider_domain, prov.requirements) - break - else: - msg = f"Unknown provider domain: {provider_domain}" - raise KeyError(msg) - if values is None: - values = self.get(f"{CONF_PROVIDERS}/{instance_id}/values", {}) if instance_id else {} - - return ( - await prov_mod.get_config_entries( - self.mass, instance_id=instance_id, action=action, values=values - ) - + DEFAULT_PROVIDER_CONFIG_ENTRIES - ) - - @api_command("config/providers/save") - async def save_provider_config( - self, - provider_domain: str, - values: dict[str, ConfigValueType], - instance_id: str | None = None, - ) -> ProviderConfig: - """ - Save Provider(instance) Config. - - provider_domain: (mandatory) domain of the provider. - values: the raw values for config entries that need to be stored/updated. - instance_id: id of an existing provider instance (None for new instance setup). - """ - if instance_id is not None: - config = await self._update_provider_config(instance_id, values) - else: - config = await self._add_provider_config(provider_domain, values) - # return full config, just in case - return await self.get_provider_config(config.instance_id) - - @api_command("config/providers/remove") - async def remove_provider_config(self, instance_id: str) -> None: - """Remove ProviderConfig.""" - conf_key = f"{CONF_PROVIDERS}/{instance_id}" - existing = self.get(conf_key) - if not existing: - msg = f"Provider {instance_id} does not exist" - raise KeyError(msg) - prov_manifest = self.mass.get_provider_manifest(existing["domain"]) - if prov_manifest.builtin: - msg = f"Builtin provider {prov_manifest.name} can not be removed." - raise RuntimeError(msg) - self.remove(conf_key) - await self.mass.unload_provider(instance_id) - if existing["type"] == "music": - # cleanup entries in library - await self.mass.music.cleanup_provider(instance_id) - if existing["type"] == "player": - # cleanup entries in player manager - for player in list(self.mass.players): - if player.provider != instance_id: - continue - self.mass.players.remove(player.player_id, cleanup_config=True) - - async def remove_provider_config_value(self, instance_id: str, key: str) -> None: - """Remove/reset single Provider config value.""" - conf_key = f"{CONF_PROVIDERS}/{instance_id}/values/{key}" - existing = self.get(conf_key) - if not existing: - return - self.remove(conf_key) - - @api_command("config/players") - async def get_player_configs( - self, provider: str | None = None, include_values: bool = False - ) -> list[PlayerConfig]: - """Return all known player configurations, optionally filtered by provider domain.""" - return [ - await self.get_player_config(raw_conf["player_id"]) - if include_values - else PlayerConfig.parse([], raw_conf) - for raw_conf in list(self.get(CONF_PLAYERS, {}).values()) - # filter out unavailable providers (only if we requested the full info) - if ( - not include_values - or raw_conf["provider"] in get_global_cache_value("available_providers", []) - ) - # optional provider filter - and (provider in (None, raw_conf["provider"])) - ] - - @api_command("config/players/get") - async def get_player_config(self, player_id: str) -> PlayerConfig: - """Return (full) configuration for a single player.""" - if raw_conf := self.get(f"{CONF_PLAYERS}/{player_id}"): - if player := self.mass.players.get(player_id, False): - raw_conf["default_name"] = player.display_name - raw_conf["provider"] = player.provider - prov = self.mass.get_provider(player.provider) - conf_entries = await prov.get_player_config_entries(player_id) - else: - # handle unavailable player and/or provider - if prov := self.mass.get_provider(raw_conf["provider"]): - conf_entries = await prov.get_player_config_entries(player_id) - else: - conf_entries = () - raw_conf["available"] = False - raw_conf["name"] = raw_conf.get("name") - raw_conf["default_name"] = raw_conf.get("default_name") or raw_conf["player_id"] - return PlayerConfig.parse(conf_entries, raw_conf) - msg = f"No config found for player id {player_id}" - raise KeyError(msg) - - @api_command("config/players/get_value") - async def get_player_config_value( - self, - player_id: str, - key: str, - ) -> ConfigValueType: - """Return single configentry value for a player.""" - conf = await self.get_player_config(player_id) - return ( - conf.values[key].value - if conf.values[key].value is not None - else conf.values[key].default_value - ) - - def get_raw_player_config_value( - self, player_id: str, key: str, default: ConfigValueType = None - ) -> ConfigValueType: - """ - Return (raw) single configentry value for a player. - - Note that this only returns the stored value without any validation or default. - """ - return self.get( - f"{CONF_PLAYERS}/{player_id}/values/{key}", - self.get(f"{CONF_PLAYERS}/{player_id}/{key}", default), - ) - - @api_command("config/players/save") - async def save_player_config( - self, player_id: str, values: dict[str, ConfigValueType] - ) -> PlayerConfig: - """Save/update PlayerConfig.""" - config = await self.get_player_config(player_id) - changed_keys = config.update(values) - if not changed_keys: - # no changes - return None - # validate/handle the update in the player manager - await self.mass.players.on_player_config_change(config, changed_keys) - # actually store changes (if the above did not raise) - conf_key = f"{CONF_PLAYERS}/{player_id}" - self.set(conf_key, config.to_raw()) - # send config updated event - self.mass.signal_event( - EventType.PLAYER_CONFIG_UPDATED, - object_id=config.player_id, - data=config, - ) - self.mass.players.update(config.player_id, force_update=True) - # return full player config (just in case) - return await self.get_player_config(player_id) - - @api_command("config/players/remove") - async def remove_player_config(self, player_id: str) -> None: - """Remove PlayerConfig.""" - conf_key = f"{CONF_PLAYERS}/{player_id}" - existing = self.get(conf_key) - if not existing: - msg = f"Player configuration for {player_id} does not exist" - raise KeyError(msg) - player = self.mass.players.get(player_id) - player_prov = player.provider if player else existing["provider"] - player_provider = self.mass.get_provider(player_prov) - if player_provider and ProviderFeature.REMOVE_PLAYER in player_provider.supported_features: - # provider supports removal of player (e.g. group player) - await player_provider.remove_player(player_id) - elif player and player_provider and player.available: - # removing a player config while it is active is not allowed - # unless the provider repoirts it has the remove_player feature (e.g. group player) - raise ActionUnavailable("Can not remove config for an active player!") - # check for group memberships that need to be updated - if player and player.active_group and player_provider: - # try to remove from the group - group_player = self.mass.players.get(player.active_group) - with suppress(UnsupportedFeaturedException, PlayerCommandFailed): - await player_provider.set_members( - player.active_group, - [x for x in group_player.group_childs if x != player.player_id], - ) - # tell the player manager to remove the player if its lingering around - # set cleanup_flag to false otherwise we end up in an infinite loop - self.mass.players.remove(player_id, cleanup_config=False) - # remove the actual config if all of the above passed - self.remove(conf_key) - - def create_default_player_config( - self, - player_id: str, - provider: str, - name: str, - enabled: bool, - values: dict[str, ConfigValueType] | None = None, - ) -> None: - """ - Create default/empty PlayerConfig. - - This is meant as helper to create default configs when a player is registered. - Called by the player manager on player register. - """ - # return early if the config already exists - if self.get(f"{CONF_PLAYERS}/{player_id}"): - # update default name if needed - if name: - self.set(f"{CONF_PLAYERS}/{player_id}/default_name", name) - return - # config does not yet exist, create a default one - conf_key = f"{CONF_PLAYERS}/{player_id}" - default_conf = PlayerConfig( - values={}, - provider=provider, - player_id=player_id, - enabled=enabled, - default_name=name, - ) - default_conf_raw = default_conf.to_raw() - if values is not None: - default_conf_raw["values"] = values - self.set( - conf_key, - default_conf_raw, - ) - - async def create_builtin_provider_config(self, provider_domain: str) -> None: - """ - Create builtin ProviderConfig. - - This is meant as helper to create default configs for builtin providers. - Called by the server initialization code which load all providers at startup. - """ - for _ in await self.get_provider_configs(provider_domain=provider_domain): - # return if there is already any config - return - for prov in self.mass.get_provider_manifests(): - if prov.domain == provider_domain: - manifest = prov - break - else: - msg = f"Unknown provider domain: {provider_domain}" - raise KeyError(msg) - 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": instance_id, - "name": manifest.name, - # note: this will only work for providers that do - # not have any required config entries or provide defaults - "values": {}, - }, - ) - default_config.validate() - conf_key = f"{CONF_PROVIDERS}/{default_config.instance_id}" - self.set(conf_key, default_config.to_raw()) - - @api_command("config/core") - async def get_core_configs(self, include_values: bool = False) -> list[CoreConfig]: - """Return all core controllers config options.""" - return [ - await self.get_core_config(core_controller) - if include_values - else CoreConfig.parse( - [], self.get(f"{CONF_CORE}/{core_controller}", {"domain": core_controller}) - ) - for core_controller in CONFIGURABLE_CORE_CONTROLLERS - ] - - @api_command("config/core/get") - async def get_core_config(self, domain: str) -> CoreConfig: - """Return configuration for a single core controller.""" - raw_conf = self.get(f"{CONF_CORE}/{domain}", {"domain": domain}) - config_entries = await self.get_core_config_entries(domain) - return CoreConfig.parse(config_entries, raw_conf) - - @api_command("config/core/get_value") - async def get_core_config_value(self, domain: str, key: str) -> ConfigValueType: - """Return single configentry value for a core controller.""" - conf = await self.get_core_config(domain) - return ( - conf.values[key].value - if conf.values[key].value is not None - else conf.values[key].default_value - ) - - @api_command("config/core/get_entries") - async def get_core_config_entries( - self, - domain: str, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to configure a core controller. - - core_controller: name of the core controller - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - if values is None: - values = self.get(f"{CONF_CORE}/{domain}/values", {}) - controller: CoreController = getattr(self.mass, domain) - return ( - await controller.get_config_entries(action=action, values=values) - + DEFAULT_CORE_CONFIG_ENTRIES - ) - - @api_command("config/core/save") - async def save_core_config( - self, - domain: str, - values: dict[str, ConfigValueType], - ) -> CoreConfig: - """Save CoreController Config values.""" - config = await self.get_core_config(domain) - changed_keys = config.update(values) - # validate the new config - config.validate() - if not changed_keys: - # no changes - return config - # try to load the provider first to catch errors before we save it. - controller: CoreController = getattr(self.mass, domain) - await controller.reload(config) - # reload succeeded, save new config - config.last_error = None - conf_key = f"{CONF_CORE}/{domain}" - self.set(conf_key, config.to_raw()) - # return full config, just in case - return await self.get_core_config(domain) - - def get_raw_core_config_value( - self, core_module: str, key: str, default: ConfigValueType = None - ) -> ConfigValueType: - """ - Return (raw) single configentry value for a core controller. - - Note that this only returns the stored value without any validation or default. - """ - return self.get( - f"{CONF_CORE}/{core_module}/values/{key}", - self.get(f"{CONF_CORE}/{core_module}/{key}", default), - ) - - def get_raw_provider_config_value( - self, provider_instance: str, key: str, default: ConfigValueType = None - ) -> ConfigValueType: - """ - Return (raw) single config(entry) value for a provider. - - Note that this only returns the stored value without any validation or default. - """ - return self.get( - f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", - self.get(f"{CONF_PROVIDERS}/{provider_instance}/{key}", default), - ) - - def set_raw_provider_config_value( - self, provider_instance: str, key: str, value: ConfigValueType, encrypted: bool = False - ) -> None: - """ - Set (raw) single config(entry) value for a provider. - - Note that this only stores the (raw) value without any validation or default. - """ - if not self.get(f"{CONF_PROVIDERS}/{provider_instance}"): - # only allow setting raw values if main entry exists - msg = f"Invalid provider_instance: {provider_instance}" - raise KeyError(msg) - if encrypted: - value = self.encrypt_string(value) - if key in BASE_KEYS: - self.set(f"{CONF_PROVIDERS}/{provider_instance}/{key}", value) - return - self.set(f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", value) - # also update the cached value in the provider itself - if prov := self.mass.get_provider(provider_instance, return_unavailable=True): - prov.config.values[key].value = value - - def set_raw_core_config_value(self, core_module: str, key: str, value: ConfigValueType) -> None: - """ - Set (raw) single config(entry) value for a core controller. - - Note that this only stores the (raw) value without any validation or default. - """ - if not self.get(f"{CONF_CORE}/{core_module}"): - # create base object first if needed - self.set(f"{CONF_CORE}/{core_module}", CoreConfig({}, core_module).to_raw()) - self.set(f"{CONF_CORE}/{core_module}/values/{key}", value) - - def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigValueType) -> None: - """ - Set (raw) single config(entry) value for a player. - - Note that this only stores the (raw) value without any validation or default. - """ - if not self.get(f"{CONF_PLAYERS}/{player_id}"): - # only allow setting raw values if main entry exists - msg = f"Invalid player_id: {player_id}" - raise KeyError(msg) - if key in BASE_KEYS: - self.set(f"{CONF_PLAYERS}/{player_id}/{key}", value) - else: - self.set(f"{CONF_PLAYERS}/{player_id}/values/{key}", value) - - def save(self, immediate: bool = False) -> None: - """Schedule save of data to disk.""" - self._value_cache = {} - if self._timer_handle is not None: - self._timer_handle.cancel() - self._timer_handle = None - - if immediate: - self.mass.loop.create_task(self._async_save()) - else: - # schedule the save for later - self._timer_handle = self.mass.loop.call_later( - DEFAULT_SAVE_DELAY, self.mass.create_task, self._async_save - ) - - def encrypt_string(self, str_value: str) -> str: - """Encrypt a (password)string with Fernet.""" - if str_value.startswith(ENCRYPT_SUFFIX): - return str_value - return ENCRYPT_SUFFIX + self._fernet.encrypt(str_value.encode()).decode() - - def decrypt_string(self, encrypted_str: str) -> str: - """Decrypt a (password)string with Fernet.""" - if not encrypted_str: - return encrypted_str - if not encrypted_str.startswith(ENCRYPT_SUFFIX): - return encrypted_str - try: - return self._fernet.decrypt(encrypted_str.replace(ENCRYPT_SUFFIX, "").encode()).decode() - except InvalidToken as err: - msg = "Password decryption failed" - raise InvalidDataError(msg) from err - - async def _load(self) -> None: - """Load data from persistent storage.""" - assert not self._data, "Already loaded" - - for filename in (self.filename, f"{self.filename}.backup"): - try: - async with aiofiles.open(filename, encoding="utf-8") as _file: - self._data = json_loads(await _file.read()) - LOGGER.debug("Loaded persistent settings from %s", filename) - await self._migrate() - return - except FileNotFoundError: - pass - except JSON_DECODE_EXCEPTIONS: - LOGGER.exception("Error while reading persistent storage file %s", filename) - LOGGER.debug("Started with empty storage: No persistent storage file found.") - - async def _migrate(self) -> None: - changed = False - - # Older versions of MA can create corrupt entries with no domain if retrying - # logic runs after a provider has been removed. Remove those corrupt entries. - for instance_id, provider_config in list(self._data.get(CONF_PROVIDERS, {}).items()): - if "domain" not in provider_config: - self._data[CONF_PROVIDERS].pop(instance_id, None) - LOGGER.warning("Removed corrupt provider configuration: %s", instance_id) - changed = True - - if changed: - await self._async_save() - - async def _async_save(self) -> None: - """Save persistent data to disk.""" - filename_backup = f"{self.filename}.backup" - # make backup before we write a new file - if await isfile(self.filename): - if await isfile(filename_backup): - await remove(filename_backup) - await rename(self.filename, filename_backup) - - async with aiofiles.open(self.filename, "w", encoding="utf-8") as _file: - await _file.write(json_dumps(self._data, indent=True)) - LOGGER.debug("Saved data to persistent storage") - - @api_command("config/providers/reload") - async def _reload_provider(self, instance_id: str) -> None: - """Reload provider.""" - try: - config = await self.get_provider_config(instance_id) - except KeyError: - # Edge case: Provider was removed before we could reload it - return - await self.mass.load_provider_config(config) - - async def _update_provider_config( - self, instance_id: str, values: dict[str, ConfigValueType] - ) -> ProviderConfig: - """Update ProviderConfig.""" - config = await self.get_provider_config(instance_id) - changed_keys = config.update(values) - available = prov.available if (prov := self.mass.get_provider(instance_id)) else False - if not changed_keys and (config.enabled == available): - # no changes - return config - # validate the new config - config.validate() - # save the config first to prevent issues when the - # provider wants to manipulate the config during load - conf_key = f"{CONF_PROVIDERS}/{config.instance_id}" - raw_conf = config.to_raw() - self.set(conf_key, raw_conf) - if config.enabled: - await self.mass.load_provider_config(config) - else: - # disable provider - prov_manifest = self.mass.get_provider_manifest(config.domain) - if not prov_manifest.allow_disable: - msg = "Provider can not be disabled." - raise RuntimeError(msg) - # also unload any other providers dependent of this provider - for dep_prov in self.mass.providers: - if dep_prov.manifest.depends_on == config.domain: - await self.mass.unload_provider(dep_prov.instance_id) - await self.mass.unload_provider(config.instance_id) - if config.type == ProviderType.PLAYER: - # cleanup entries in player manager - for player in self.mass.players.all(return_unavailable=True, return_disabled=True): - if player.provider != instance_id: - continue - self.mass.players.remove(player.player_id, cleanup_config=False) - return config - - async def _add_provider_config( - self, - provider_domain: str, - values: dict[str, ConfigValueType], - ) -> list[ConfigEntry] | ProviderConfig: - """ - Add new Provider (instance). - - params: - - provider_domain: domain of the provider for which to add an instance of. - - values: the raw values for config entries. - - Returns: newly created ProviderConfig. - """ - # lookup provider manifest and module - for prov in self.mass.get_provider_manifests(): - if prov.domain == provider_domain: - manifest = prov - break - else: - msg = f"Unknown provider domain: {provider_domain}" - raise KeyError(msg) - if prov.depends_on and not self.mass.get_provider(prov.depends_on): - msg = f"Provider {manifest.name} depends on {prov.depends_on}" - raise ValueError(msg) - # create new provider config with given values - existing = { - x.instance_id for x in await self.get_provider_configs(provider_domain=provider_domain) - } - # determine instance id based on previous configs - if existing and not manifest.multi_instance: - msg = f"Provider {manifest.name} does not support multiple instances" - raise ValueError(msg) - 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 - ) - config: ProviderConfig = ProviderConfig.parse( - config_entries, - { - "type": manifest.type.value, - "domain": manifest.domain, - "instance_id": instance_id, - "name": manifest.name, - "values": values, - }, - ) - # validate the new config - config.validate() - # save the config first to prevent issues when the - # provider wants to manipulate the config during load - conf_key = f"{CONF_PROVIDERS}/{config.instance_id}" - self.set(conf_key, config.to_raw()) - # try to load the provider - try: - await self.mass.load_provider_config(config) - except Exception: - # loading failed, remove config - self.remove(conf_key) - raise - return config diff --git a/music_assistant/server/controllers/media/__init__.py b/music_assistant/server/controllers/media/__init__.py deleted file mode 100644 index 3256b9e0..00000000 --- a/music_assistant/server/controllers/media/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package with Media controllers.""" diff --git a/music_assistant/server/controllers/media/albums.py b/music_assistant/server/controllers/media/albums.py deleted file mode 100644 index a2733168..00000000 --- a/music_assistant/server/controllers/media/albums.py +++ /dev/null @@ -1,519 +0,0 @@ -"""Manage MediaItems of type Album.""" - -from __future__ import annotations - -import contextlib -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any - -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import CacheCategory, ProviderFeature -from music_assistant.common.models.errors import ( - InvalidDataError, - MediaNotFoundError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - ItemMapping, - MediaType, - Track, - UniqueList, -) -from music_assistant.constants import DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS -from music_assistant.server.controllers.media.base import MediaControllerBase -from music_assistant.server.helpers.compare import ( - compare_album, - compare_artists, - compare_media_item, - loose_compare_strings, -) - -if TYPE_CHECKING: - from music_assistant.server.models.music_provider import MusicProvider - - -class AlbumsController(MediaControllerBase[Album]): - """Controller managing MediaItems of type Album.""" - - db_table = DB_TABLE_ALBUMS - media_type = MediaType.ALBUM - item_cls = Album - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - self.base_query = """ - SELECT - albums.*, - (SELECT JSON_GROUP_ARRAY( - json_object( - 'item_id', provider_mappings.provider_item_id, - 'provider_domain', provider_mappings.provider_domain, - 'provider_instance', provider_mappings.provider_instance, - 'available', provider_mappings.available, - 'audio_format', json(provider_mappings.audio_format), - 'url', provider_mappings.url, - 'details', provider_mappings.details - )) FROM provider_mappings WHERE provider_mappings.item_id = albums.item_id AND media_type = 'album') AS provider_mappings, - (SELECT JSON_GROUP_ARRAY( - json_object( - 'item_id', artists.item_id, - 'provider', 'library', - 'name', artists.name, - 'sort_name', artists.sort_name, - 'media_type', 'artist' - )) FROM artists JOIN album_artists on album_artists.album_id = albums.item_id WHERE artists.item_id = album_artists.artist_id) AS artists - FROM albums""" # noqa: E501 - # register (extra) api handlers - api_base = self.api_base - self.mass.register_api_command(f"music/{api_base}/album_tracks", self.tracks) - self.mass.register_api_command(f"music/{api_base}/album_versions", self.versions) - - async def get( - self, - item_id: str, - provider_instance_id_or_domain: str, - recursive: bool = True, - ) -> Album: - """Return (full) details for a single media item.""" - album = await super().get( - item_id, - provider_instance_id_or_domain, - ) - if not recursive: - return album - - # append artist details to full album item (resolve ItemMappings) - album_artists = UniqueList() - for artist in album.artists: - if not isinstance(artist, ItemMapping): - album_artists.append(artist) - continue - with contextlib.suppress(MediaNotFoundError): - album_artists.append( - await self.mass.music.artists.get( - artist.item_id, - artist.provider, - ) - ) - album.artists = album_artists - return album - - async def library_items( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int = 500, - offset: int = 0, - order_by: str = "sort_name", - provider: str | None = None, - extra_query: str | None = None, - extra_query_params: dict[str, Any] | None = None, - album_types: list[AlbumType] | None = None, - ) -> list[Artist]: - """Get in-database albums.""" - extra_query_params: dict[str, Any] = extra_query_params or {} - extra_query_parts: list[str] = [extra_query] if extra_query else [] - extra_join_parts: list[str] = [] - artist_table_joined = False - # optional album type filter - if album_types: - extra_query_parts.append("albums.album_type IN :album_types") - extra_query_params["album_types"] = [x.value for x in album_types] - if order_by and "artist_name" in order_by: - # join artist table to allow sorting on artist name - extra_join_parts.append( - "JOIN album_artists ON album_artists.album_id = albums.item_id " - "JOIN artists ON artists.item_id = album_artists.artist_id " - ) - artist_table_joined = True - if search and " - " in search: - # handle combined artist + title search - artist_str, title_str = search.split(" - ", 1) - search = None - extra_query_parts.append("albums.name LIKE :search_title") - extra_query_params["search_title"] = f"%{title_str}%" - # use join with artists table to filter on artist name - extra_join_parts.append( - "JOIN album_artists ON album_artists.album_id = albums.item_id " - "JOIN artists ON artists.item_id = album_artists.artist_id " - "AND artists.name LIKE :search_artist" - if not artist_table_joined - else "AND artists.name LIKE :search_artist" - ) - artist_table_joined = True - extra_query_params["search_artist"] = f"%{artist_str}%" - result = await self._get_library_items_by_query( - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - provider=provider, - extra_query_parts=extra_query_parts, - extra_query_params=extra_query_params, - extra_join_parts=extra_join_parts, - ) - if search and len(result) < 25 and not offset: - # append artist items to result - extra_join_parts.append( - "JOIN album_artists ON album_artists.album_id = albums.item_id " - "JOIN artists ON artists.item_id = album_artists.artist_id " - "AND artists.name LIKE :search_artist" - if not artist_table_joined - else "AND artists.name LIKE :search_artist" - ) - extra_query_params["search_artist"] = f"%{search}%" - return result + await self._get_library_items_by_query( - favorite=favorite, - search=None, - limit=limit, - order_by=order_by, - provider=provider, - extra_query_parts=extra_query_parts, - extra_query_params=extra_query_params, - extra_join_parts=extra_join_parts, - ) - return result - - async def library_count( - self, favorite_only: bool = False, album_types: list[AlbumType] | None = None - ) -> int: - """Return the total number of items in the library.""" - sql_query = f"SELECT item_id FROM {self.db_table}" - query_parts: list[str] = [] - query_params: dict[str, Any] = {} - if favorite_only: - query_parts.append("favorite = 1") - if album_types: - query_parts.append("albums.album_type IN :album_types") - query_params["album_types"] = [x.value for x in album_types] - if query_parts: - sql_query += f" WHERE {' AND '.join(query_parts)}" - return await self.mass.music.database.get_count_from_query(sql_query, query_params) - - async def remove_item_from_library(self, item_id: str | int) -> None: - """Delete record from the database.""" - db_id = int(item_id) # ensure integer - # recursively also remove album tracks - for db_track in await self.get_library_album_tracks(db_id): - with contextlib.suppress(MediaNotFoundError): - await self.mass.music.tracks.remove_item_from_library(db_track.item_id) - # delete entry(s) from albumtracks table - await self.mass.music.database.delete(DB_TABLE_ALBUM_TRACKS, {"album_id": db_id}) - # delete entry(s) from album artists table - await self.mass.music.database.delete(DB_TABLE_ALBUM_ARTISTS, {"album_id": db_id}) - # delete the album itself from db - await super().remove_item_from_library(item_id) - - async def tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> UniqueList[Track]: - """Return album tracks for the given provider album id.""" - # always check if we have a library item for this album - library_album = await self.get_library_item_by_prov_id( - item_id, provider_instance_id_or_domain - ) - if not library_album: - return await self._get_provider_album_tracks(item_id, provider_instance_id_or_domain) - db_items = await self.get_library_album_tracks(library_album.item_id) - result: UniqueList[Track] = UniqueList(db_items) - if in_library_only: - # return in-library items only - return sorted(db_items, key=lambda x: (x.disc_number, x.track_number)) - - # return all (unique) items from all providers - # because we are returning the items from all providers combined, - # we need to make sure that we don't return duplicates - unique_ids: set[str] = {f"{x.disc_number}.{x.track_number}" for x in db_items} - unique_ids.update({f"{x.name.lower()}.{x.version.lower()}" for x in db_items}) - for db_item in db_items: - unique_ids.add(x.item_id for x in db_item.provider_mappings) - for provider_mapping in library_album.provider_mappings: - provider_tracks = await self._get_provider_album_tracks( - provider_mapping.item_id, provider_mapping.provider_instance - ) - for provider_track in provider_tracks: - if provider_track.item_id in unique_ids: - continue - unique_id = f"{provider_track.disc_number}.{provider_track.track_number}" - if unique_id in unique_ids: - continue - unique_id = f"{provider_track.name.lower()}.{provider_track.version.lower()}" - if unique_id in unique_ids: - continue - unique_ids.add(unique_id) - provider_track.album = library_album - result.append(provider_track) - # NOTE: we need to return the results sorted on disc/track here - # to ensure the correct order at playback - return sorted(result, key=lambda x: (x.disc_number, x.track_number)) - - async def versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> UniqueList[Album]: - """Return all versions of an album we can find on all providers.""" - album = await self.get_provider_item(item_id, provider_instance_id_or_domain) - search_query = f"{album.artists[0].name} - {album.name}" if album.artists else album.name - result: UniqueList[Album] = UniqueList() - for provider_id in self.mass.music.get_unique_providers(): - provider = self.mass.get_provider(provider_id) - if not provider: - continue - if not provider.library_supported(MediaType.ALBUM): - continue - result.extend( - prov_item - for prov_item in await self.search(search_query, provider_id) - if loose_compare_strings(album.name, prov_item.name) - and compare_artists(prov_item.artists, album.artists, any_match=True) - # make sure that the 'base' version is NOT included - and not album.provider_mappings.intersection(prov_item.provider_mappings) - ) - return result - - async def get_library_album_tracks( - self, - item_id: str | int, - ) -> list[Track]: - """Return in-database album tracks for the given database album.""" - return await self.mass.music.tracks._get_library_items_by_query( - extra_query_parts=[f"WHERE album_tracks.album_id = {item_id}"], - ) - - async def _add_library_item(self, item: Album) -> int: - """Add a new record to the database.""" - if not isinstance(item, Album): - msg = "Not a valid Album object (ItemMapping can not be added to db)" - raise InvalidDataError(msg) - if not item.artists: - msg = "Album is missing artist(s)" - raise InvalidDataError(msg) - db_id = await self.mass.music.database.insert( - self.db_table, - { - "name": item.name, - "sort_name": item.sort_name, - "version": item.version, - "favorite": item.favorite, - "album_type": item.album_type, - "year": item.year, - "metadata": serialize_to_json(item.metadata), - "external_ids": serialize_to_json(item.external_ids), - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, item.provider_mappings) - # set track artist(s) - await self._set_album_artists(db_id, item.artists) - self.logger.debug("added %s to database (id: %s)", item.name, db_id) - return db_id - - async def _update_library_item( - self, item_id: str | int, update: Album, overwrite: bool = False - ) -> None: - """Update existing record in the database.""" - db_id = int(item_id) # ensure integer - cur_item = await self.get_library_item(db_id) - metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) - if getattr(update, "album_type", AlbumType.UNKNOWN) != AlbumType.UNKNOWN: - album_type = update.album_type - else: - album_type = cur_item.album_type - cur_item.external_ids.update(update.external_ids) - provider_mappings = ( - update.provider_mappings - if overwrite - else {*cur_item.provider_mappings, *update.provider_mappings} - ) - await self.mass.music.database.update( - self.db_table, - {"item_id": db_id}, - { - "name": update.name if overwrite else cur_item.name, - "sort_name": update.sort_name - if overwrite - else cur_item.sort_name or update.sort_name, - "version": update.version if overwrite else cur_item.version or update.version, - "year": update.year if overwrite else cur_item.year or update.year, - "album_type": album_type.value, - "metadata": serialize_to_json(metadata), - "external_ids": serialize_to_json( - update.external_ids if overwrite else cur_item.external_ids - ), - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, provider_mappings, overwrite) - # set album artist(s) - artists = update.artists if overwrite else cur_item.artists + update.artists - await self._set_album_artists(db_id, artists, overwrite=overwrite) - self.logger.debug("updated %s in database: (id %s)", update.name, db_id) - - async def _get_provider_album_tracks( - self, item_id: str, provider_instance_id_or_domain: str - ) -> list[Track]: - """Return album tracks for the given provider album id.""" - prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain) - if prov is None: - return [] - # prefer cache items (if any) - for streaming providers only - cache_category = CacheCategory.MUSIC_ALBUM_TRACKS - cache_base_key = prov.lookup_key - cache_key = item_id - if ( - prov.is_streaming_provider - and ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ) - is not None - ): - return [Track.from_dict(x) for x in cache] - # no items in cache - get listing from provider - items = await prov.get_album_tracks(item_id) - # store (serializable items) in cache - if prov.is_streaming_provider: - self.mass.create_task( - self.mass.cache.set(cache_key, [x.to_dict() for x in items]), - category=cache_category, - base_key=cache_base_key, - ) - for item in items: - # if this is a complete track object, pre-cache it as - # that will save us an (expensive) lookup later - if item.image and item.artist_str and item.album and prov.domain != "builtin": - await self.mass.cache.set( - f"track.{item_id}", - item.to_dict(), - category=CacheCategory.MUSIC_PROVIDER_ITEM, - base_key=prov.lookup_key, - ) - return items - - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ): - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - assert provider_instance_id_or_domain != "library" - return await self._get_provider_album_tracks(item_id, provider_instance_id_or_domain) - - async def _get_dynamic_tracks( - self, - media_item: Album, - limit: int = 25, - ) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) - msg = "No Music Provider found that supports requesting similar tracks." - raise UnsupportedFeaturedException(msg) - - async def _set_album_artists( - self, db_id: int, artists: Iterable[Artist | ItemMapping], overwrite: bool = False - ) -> None: - """Store Album Artists.""" - if overwrite: - # on overwrite, clear the album_artists table first - await self.mass.music.database.delete( - DB_TABLE_ALBUM_ARTISTS, - { - "album_id": db_id, - }, - ) - for artist in artists: - await self._set_album_artist(db_id, artist=artist, overwrite=overwrite) - - async def _set_album_artist( - self, db_id: int, artist: Artist | ItemMapping, overwrite: bool = False - ) -> ItemMapping: - """Store Album Artist info.""" - db_artist: Artist | ItemMapping = None - if artist.provider == "library": - db_artist = artist - elif existing := await self.mass.music.artists.get_library_item_by_prov_id( - artist.item_id, artist.provider - ): - db_artist = existing - - if not db_artist or overwrite: - db_artist = await self.mass.music.artists.add_item_to_library( - artist, overwrite_existing=overwrite - ) - # write (or update) record in album_artists table - await self.mass.music.database.insert_or_replace( - DB_TABLE_ALBUM_ARTISTS, - { - "album_id": db_id, - "artist_id": int(db_artist.item_id), - }, - ) - return ItemMapping.from_item(db_artist) - - async def match_providers(self, db_album: Album) -> None: - """Try to find match on all (streaming) providers for the provided (database) album. - - This is used to link objects of different providers/qualities together. - """ - if db_album.provider != "library": - return # Matching only supported for database items - if not db_album.artists: - return # guard - artist_name = db_album.artists[0].name - - async def find_prov_match(provider: MusicProvider): - self.logger.debug( - "Trying to match album %s on provider %s", db_album.name, provider.name - ) - match_found = False - search_str = f"{artist_name} - {db_album.name}" - search_result = await self.search(search_str, provider.instance_id) - for search_result_item in search_result: - if not search_result_item.available: - continue - if not compare_media_item(db_album, search_result_item): - continue - # we must fetch the full album version, search results can be simplified objects - prov_album = await self.get_provider_item( - search_result_item.item_id, - search_result_item.provider, - fallback=search_result_item, - ) - if compare_album(db_album, prov_album): - # 100% match, we update the db with the additional provider mapping(s) - match_found = True - for provider_mapping in search_result_item.provider_mappings: - await self.add_provider_mapping(db_album.item_id, provider_mapping) - db_album.provider_mappings.add(provider_mapping) - return match_found - - # try to find match on all providers - cur_provider_domains = {x.provider_domain for x in db_album.provider_mappings} - for provider in self.mass.music.providers: - if provider.domain in cur_provider_domains: - continue - if ProviderFeature.SEARCH not in provider.supported_features: - continue - if not provider.library_supported(MediaType.ALBUM): - continue - if not provider.is_streaming_provider: - # matching on unique providers is pointless as they push (all) their content to MA - continue - if await find_prov_match(provider): - cur_provider_domains.add(provider.domain) - else: - self.logger.debug( - "Could not find match for Album %s on provider %s", - db_album.name, - provider.name, - ) diff --git a/music_assistant/server/controllers/media/artists.py b/music_assistant/server/controllers/media/artists.py deleted file mode 100644 index ce419192..00000000 --- a/music_assistant/server/controllers/media/artists.py +++ /dev/null @@ -1,544 +0,0 @@ -"""Manage MediaItems of type Artist.""" - -from __future__ import annotations - -import asyncio -import contextlib -from typing import TYPE_CHECKING, Any - -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import CacheCategory, ProviderFeature -from music_assistant.common.models.errors import ( - MediaNotFoundError, - ProviderUnavailableError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - ItemMapping, - MediaType, - Track, - UniqueList, -) -from music_assistant.constants import ( - DB_TABLE_ALBUM_ARTISTS, - DB_TABLE_ARTISTS, - DB_TABLE_TRACK_ARTISTS, - VARIOUS_ARTISTS_MBID, - VARIOUS_ARTISTS_NAME, -) -from music_assistant.server.controllers.media.base import MediaControllerBase -from music_assistant.server.helpers.compare import compare_artist, compare_strings - -if TYPE_CHECKING: - from music_assistant.server.models.music_provider import MusicProvider - - -class ArtistsController(MediaControllerBase[Artist]): - """Controller managing MediaItems of type Artist.""" - - db_table = DB_TABLE_ARTISTS - media_type = MediaType.ARTIST - item_cls = Artist - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - self._db_add_lock = asyncio.Lock() - # register (extra) api handlers - api_base = self.api_base - self.mass.register_api_command(f"music/{api_base}/artist_albums", self.albums) - self.mass.register_api_command(f"music/{api_base}/artist_tracks", self.tracks) - - async def library_count( - self, favorite_only: bool = False, album_artists_only: bool = False - ) -> int: - """Return the total number of items in the library.""" - sql_query = f"SELECT item_id FROM {self.db_table}" - query_parts: list[str] = [] - if favorite_only: - query_parts.append("favorite = 1") - if album_artists_only: - query_parts.append( - f"item_id in (select {DB_TABLE_ALBUM_ARTISTS}.artist_id " - f"FROM {DB_TABLE_ALBUM_ARTISTS})" - ) - if query_parts: - sql_query += f" WHERE {' AND '.join(query_parts)}" - return await self.mass.music.database.get_count_from_query(sql_query) - - async def library_items( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int = 500, - offset: int = 0, - order_by: str = "sort_name", - provider: str | None = None, - extra_query: str | None = None, - extra_query_params: dict[str, Any] | None = None, - album_artists_only: bool = False, - ) -> list[Artist]: - """Get in-database (album) artists.""" - extra_query_params: dict[str, Any] = extra_query_params or {} - extra_query_parts: list[str] = [extra_query] if extra_query else [] - if album_artists_only: - extra_query_parts.append( - f"artists.item_id in (select {DB_TABLE_ALBUM_ARTISTS}.artist_id " - f"from {DB_TABLE_ALBUM_ARTISTS})" - ) - return await self._get_library_items_by_query( - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - provider=provider, - extra_query_parts=extra_query_parts, - extra_query_params=extra_query_params, - ) - - async def tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> UniqueList[Track]: - """Return all/top tracks for an artist.""" - # always check if we have a library item for this artist - library_artist = await self.get_library_item_by_prov_id( - item_id, provider_instance_id_or_domain - ) - if not library_artist: - return await self.get_provider_artist_toptracks(item_id, provider_instance_id_or_domain) - db_items = await self.get_library_artist_tracks(library_artist.item_id) - result: UniqueList[Track] = UniqueList(db_items) - if in_library_only: - # return in-library items only - return result - # return all (unique) items from all providers - unique_ids: set[str] = set() - for provider_mapping in library_artist.provider_mappings: - provider_tracks = await self.get_provider_artist_toptracks( - provider_mapping.item_id, provider_mapping.provider_instance - ) - for provider_track in provider_tracks: - unique_id = f"{provider_track.name}.{provider_track.version}" - if unique_id in unique_ids: - continue - unique_ids.add(unique_id) - # prefer db item - if db_item := await self.mass.music.tracks.get_library_item_by_prov_id( - provider_track.item_id, provider_track.provider - ): - result.append(db_item) - elif not in_library_only: - result.append(provider_track) - return result - - async def albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> UniqueList[Album]: - """Return (all/most popular) albums for an artist.""" - # always check if we have a library item for this artist - library_artist = await self.get_library_item_by_prov_id( - item_id, provider_instance_id_or_domain - ) - if not library_artist: - return await self.get_provider_artist_albums(item_id, provider_instance_id_or_domain) - db_items = await self.get_library_artist_albums(library_artist.item_id) - result: UniqueList[Album] = UniqueList(db_items) - if in_library_only: - # return in-library items only - return result - # return all (unique) items from all providers - unique_ids: set[str] = set() - for provider_mapping in library_artist.provider_mappings: - provider_albums = await self.get_provider_artist_albums( - provider_mapping.item_id, provider_mapping.provider_instance - ) - for provider_album in provider_albums: - unique_id = f"{provider_album.name}.{provider_album.version}" - if unique_id in unique_ids: - continue - unique_ids.add(unique_id) - # prefer db item - if db_item := await self.mass.music.albums.get_library_item_by_prov_id( - provider_album.item_id, provider_album.provider - ): - result.append(db_item) - elif not in_library_only: - result.append(provider_album) - return result - - async def remove_item_from_library(self, item_id: str | int) -> None: - """Delete record from the database.""" - db_id = int(item_id) # ensure integer - # recursively also remove artist albums - for db_row in await self.mass.music.database.get_rows_from_query( - f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {db_id}", - limit=5000, - ): - with contextlib.suppress(MediaNotFoundError): - await self.mass.music.albums.remove_item_from_library(db_row["album_id"]) - - # recursively also remove artist tracks - for db_row in await self.mass.music.database.get_rows_from_query( - f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {db_id}", - limit=5000, - ): - with contextlib.suppress(MediaNotFoundError): - await self.mass.music.tracks.remove_item_from_library(db_row["track_id"]) - - # delete the artist itself from db - await super().remove_item_from_library(db_id) - - async def get_provider_artist_toptracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Track]: - """Return top tracks for an artist on given provider.""" - items = [] - assert provider_instance_id_or_domain != "library" - prov = self.mass.get_provider(provider_instance_id_or_domain) - if prov is None: - return [] - # prefer cache items (if any) - for streaming providers - cache_category = CacheCategory.MUSIC_ARTIST_TRACKS - cache_base_key = prov.lookup_key - cache_key = item_id - if ( - prov.is_streaming_provider - and ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ) - is not None - ): - return [Track.from_dict(x) for x in cache] - # no items in cache - get listing from provider - if ProviderFeature.ARTIST_TOPTRACKS in prov.supported_features: - items = await prov.get_artist_toptracks(item_id) - for item in items: - # if this is a complete track object, pre-cache it as - # that will save us an (expensive) lookup later - if item.image and item.artist_str and item.album and prov.domain != "builtin": - await self.mass.cache.set( - f"track.{item_id}", - item.to_dict(), - category=CacheCategory.MUSIC_PROVIDER_ITEM, - base_key=prov.lookup_key, - ) - else: - # fallback implementation using the db - if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( - item_id, - provider_instance_id_or_domain, - ): - artist_id = db_artist.item_id - subquery = ( - f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {artist_id}" - ) - query = f"tracks.item_id in ({subquery})" - return await self.mass.music.tracks._get_library_items_by_query( - extra_query_parts=[query], provider=provider_instance_id_or_domain - ) - # store (serializable items) in cache - if prov.is_streaming_provider: - self.mass.create_task( - self.mass.cache.set( - cache_key, - [x.to_dict() for x in items], - category=cache_category, - base_key=cache_base_key, - ) - ) - return items - - async def get_library_artist_tracks( - self, - item_id: str | int, - ) -> list[Track]: - """Return all tracks for an artist in the library/db.""" - subquery = f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {item_id}" - query = f"tracks.item_id in ({subquery})" - return await self.mass.music.tracks._get_library_items_by_query(extra_query_parts=[query]) - - async def get_provider_artist_albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Album]: - """Return albums for an artist on given provider.""" - items = [] - assert provider_instance_id_or_domain != "library" - prov = self.mass.get_provider(provider_instance_id_or_domain) - if prov is None: - return [] - # prefer cache items (if any) - cache_category = CacheCategory.MUSIC_ARTIST_ALBUMS - cache_base_key = prov.lookup_key - cache_key = item_id - if ( - prov.is_streaming_provider - and ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ) - is not None - ): - return [Album.from_dict(x) for x in cache] - # no items in cache - get listing from provider - if ProviderFeature.ARTIST_ALBUMS in prov.supported_features: - items = await prov.get_artist_albums(item_id) - else: - # fallback implementation using the db - # ruff: noqa: PLR5501 - if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( - item_id, - provider_instance_id_or_domain, - ): - artist_id = db_artist.item_id - subquery = ( - f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {artist_id}" - ) - query = f"albums.item_id in ({subquery})" - return await self.mass.music.albums._get_library_items_by_query( - extra_query_parts=[query], provider=provider_instance_id_or_domain - ) - - # store (serializable items) in cache - if prov.is_streaming_provider: - self.mass.create_task( - self.mass.cache.set( - cache_key, - [x.to_dict() for x in items], - category=cache_category, - base_key=cache_base_key, - ) - ) - return items - - async def get_library_artist_albums( - self, - item_id: str | int, - ) -> list[Album]: - """Return all in-library albums for an artist.""" - subquery = f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {item_id}" - query = f"albums.item_id in ({subquery})" - return await self.mass.music.albums._get_library_items_by_query(extra_query_parts=[query]) - - async def _add_library_item(self, item: Artist | ItemMapping) -> int: - """Add a new item record to the database.""" - if isinstance(item, ItemMapping): - item = self._artist_from_item_mapping(item) - # enforce various artists name + id - if compare_strings(item.name, VARIOUS_ARTISTS_NAME): - item.mbid = VARIOUS_ARTISTS_MBID - if item.mbid == VARIOUS_ARTISTS_MBID: - item.name = VARIOUS_ARTISTS_NAME - # no existing item matched: insert item - db_id = await self.mass.music.database.insert( - self.db_table, - { - "name": item.name, - "sort_name": item.sort_name, - "favorite": item.favorite, - "external_ids": serialize_to_json(item.external_ids), - "metadata": serialize_to_json(item.metadata), - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, item.provider_mappings) - self.logger.debug("added %s to database (id: %s)", item.name, db_id) - return db_id - - async def _update_library_item( - self, item_id: str | int, update: Artist | ItemMapping, overwrite: bool = False - ) -> None: - """Update existing record in the database.""" - db_id = int(item_id) # ensure integer - cur_item = await self.get_library_item(db_id) - if isinstance(update, ItemMapping): - # NOTE that artist is the only mediatype where its accepted we - # receive an itemmapping from streaming providers - update = self._artist_from_item_mapping(update) - metadata = cur_item.metadata - else: - metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) - cur_item.external_ids.update(update.external_ids) - # enforce various artists name + id - mbid = cur_item.mbid - if (not mbid or overwrite) and getattr(update, "mbid", None): - if compare_strings(update.name, VARIOUS_ARTISTS_NAME): - update.mbid = VARIOUS_ARTISTS_MBID - if update.mbid == VARIOUS_ARTISTS_MBID: - update.name = VARIOUS_ARTISTS_NAME - - await self.mass.music.database.update( - self.db_table, - {"item_id": db_id}, - { - "name": update.name if overwrite else cur_item.name, - "sort_name": update.sort_name - if overwrite - else cur_item.sort_name or update.sort_name, - "external_ids": serialize_to_json( - update.external_ids if overwrite else cur_item.external_ids - ), - "metadata": serialize_to_json(metadata), - }, - ) - self.logger.debug("updated %s in database: %s", update.name, db_id) - # update/set provider_mappings table - provider_mappings = ( - update.provider_mappings - if overwrite - else {*cur_item.provider_mappings, *update.provider_mappings} - ) - await self._set_provider_mappings(db_id, provider_mappings, overwrite) - self.logger.debug("updated %s in database: (id %s)", update.name, db_id) - - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ): - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - assert provider_instance_id_or_domain != "library" - return await self.get_provider_artist_toptracks( - item_id, - provider_instance_id_or_domain, - ) - - async def _get_dynamic_tracks( - self, - media_item: Artist, - limit: int = 25, - ) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) - msg = "No Music Provider found that supports requesting similar tracks." - raise UnsupportedFeaturedException(msg) - - async def match_providers(self, db_artist: Artist) -> None: - """Try to find matching artists on all providers for the provided (database) item_id. - - This is used to link objects of different providers together. - """ - assert db_artist.provider == "library", "Matching only supported for database items!" - cur_provider_domains = {x.provider_domain for x in db_artist.provider_mappings} - for provider in self.mass.music.providers: - if provider.domain in cur_provider_domains: - continue - if ProviderFeature.SEARCH not in provider.supported_features: - continue - if not provider.library_supported(MediaType.ARTIST): - continue - if not provider.is_streaming_provider: - # matching on unique providers is pointless as they push (all) their content to MA - continue - if await self._match_provider(db_artist, provider): - cur_provider_domains.add(provider.domain) - else: - self.logger.debug( - "Could not find match for Artist %s on provider %s", - db_artist.name, - provider.name, - ) - - async def _match_provider(self, db_artist: Artist, provider: MusicProvider) -> bool: - """Try to find matching artists on given provider for the provided (database) artist.""" - self.logger.debug("Trying to match artist %s on provider %s", db_artist.name, provider.name) - # try to get a match with some reference tracks of this artist - ref_tracks = await self.mass.music.artists.tracks(db_artist.item_id, db_artist.provider) - if len(ref_tracks) < 10: - # fetch reference tracks from provider(s) attached to the artist - for provider_mapping in db_artist.provider_mappings: - with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError): - ref_tracks += await self.mass.music.artists.tracks( - provider_mapping.item_id, provider_mapping.provider_instance - ) - for ref_track in ref_tracks: - search_str = f"{db_artist.name} - {ref_track.name}" - search_results = await self.mass.music.tracks.search(search_str, provider.domain) - for search_result_item in search_results: - if not compare_strings(search_result_item.name, ref_track.name, strict=True): - continue - # get matching artist from track - for search_item_artist in search_result_item.artists: - if not compare_strings(search_item_artist.name, db_artist.name, strict=True): - continue - # 100% track match - # get full artist details so we have all metadata - prov_artist = await self.get_provider_item( - search_item_artist.item_id, - search_item_artist.provider, - fallback=search_result_item, - ) - # 100% match, we update the db with the additional provider mapping(s) - for provider_mapping in prov_artist.provider_mappings: - await self.add_provider_mapping(db_artist.item_id, provider_mapping) - db_artist.provider_mappings.add(provider_mapping) - return True - # try to get a match with some reference albums of this artist - ref_albums = await self.mass.music.artists.albums(db_artist.item_id, db_artist.provider) - if len(ref_albums) < 10: - # fetch reference albums from provider(s) attached to the artist - for provider_mapping in db_artist.provider_mappings: - with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError): - ref_albums += await self.mass.music.artists.albums( - provider_mapping.item_id, provider_mapping.provider_instance - ) - for ref_album in ref_albums: - if ref_album.album_type == AlbumType.COMPILATION: - continue - if not ref_album.artists: - continue - search_str = f"{db_artist.name} - {ref_album.name}" - search_result = await self.mass.music.albums.search(search_str, provider.domain) - for search_result_item in search_result: - if not search_result_item.artists: - continue - if not compare_strings(search_result_item.name, ref_album.name): - continue - # artist must match 100% - if not compare_artist(db_artist, search_result_item.artists[0]): - continue - # 100% match - # get full artist details so we have all metadata - prov_artist = await self.get_provider_item( - search_result_item.artists[0].item_id, - search_result_item.artists[0].provider, - fallback=search_result_item.artists[0], - ) - await self._update_library_item(db_artist.item_id, prov_artist) - return True - return False - - def _artist_from_item_mapping(self, item: ItemMapping) -> Artist: - domain, instance_id = None, None - if prov := self.mass.get_provider(item.provider): - domain = prov.domain - instance_id = prov.instance_id - return Artist.from_dict( - { - **item.to_dict(), - "provider_mappings": [ - { - "item_id": item.item_id, - "provider_domain": domain, - "provider_instance": instance_id, - "available": item.available, - } - ], - } - ) diff --git a/music_assistant/server/controllers/media/base.py b/music_assistant/server/controllers/media/base.py deleted file mode 100644 index ddad990d..00000000 --- a/music_assistant/server/controllers/media/base.py +++ /dev/null @@ -1,818 +0,0 @@ -"""Base (ABC) MediaType specific controller.""" - -from __future__ import annotations - -import asyncio -import logging -from abc import ABCMeta, abstractmethod -from collections.abc import Iterable -from contextlib import suppress -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -from music_assistant.common.helpers.json import json_loads, serialize_to_json -from music_assistant.common.models.enums import ( - CacheCategory, - EventType, - ExternalID, - MediaType, - ProviderFeature, -) -from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError -from music_assistant.common.models.media_items import ( - Album, - ItemMapping, - MediaItemType, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PROVIDER_MAPPINGS, MASS_LOGGER_NAME -from music_assistant.server.helpers.compare import compare_media_item - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Mapping - - from music_assistant.server import MusicAssistant - -ItemCls = TypeVar("ItemCls", bound="MediaItemType") - -JSON_KEYS = ("artists", "track_album", "metadata", "provider_mappings", "external_ids") - -SORT_KEYS = { - "name": "name COLLATE NOCASE ASC", - "name_desc": "name COLLATE NOCASE DESC", - "sort_name": "sort_name COLLATE NOCASE ASC", - "sort_name_desc": "sort_name COLLATE NOCASE DESC", - "timestamp_added": "timestamp_added ASC", - "timestamp_added_desc": "timestamp_added DESC", - "timestamp_modified": "timestamp_modified ASC", - "timestamp_modified_desc": "timestamp_modified DESC", - "last_played": "last_played ASC", - "last_played_desc": "last_played DESC", - "play_count": "play_count ASC", - "play_count_desc": "play_count DESC", - "year": "year ASC", - "year_desc": "year DESC", - "position": "position ASC", - "position_desc": "position DESC", - "artist_name": "artists.name COLLATE NOCASE ASC", - "artist_name_desc": "artists.name COLLATE NOCASE DESC", - "random": "RANDOM()", - "random_play_count": "RANDOM(), play_count ASC", -} - - -class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): - """Base model for controller managing a MediaType.""" - - media_type: MediaType - item_cls: MediaItemType - db_table: str - - def __init__(self, mass: MusicAssistant) -> None: - """Initialize class.""" - self.mass = mass - self.base_query = f""" - SELECT - {self.db_table}.*, - (SELECT JSON_GROUP_ARRAY( - json_object( - 'item_id', provider_mappings.provider_item_id, - 'provider_domain', provider_mappings.provider_domain, - 'provider_instance', provider_mappings.provider_instance, - 'available', provider_mappings.available, - 'audio_format', json(provider_mappings.audio_format), - 'url', provider_mappings.url, - 'details', provider_mappings.details - )) FROM provider_mappings WHERE provider_mappings.item_id = {self.db_table}.item_id - AND provider_mappings.media_type = '{self.media_type.value}') AS provider_mappings - FROM {self.db_table} """ # noqa: E501 - self.logger = logging.getLogger(f"{MASS_LOGGER_NAME}.music.{self.media_type.value}") - # register (base) api handlers - self.api_base = api_base = f"{self.media_type}s" - self.mass.register_api_command(f"music/{api_base}/count", self.library_count) - self.mass.register_api_command(f"music/{api_base}/library_items", self.library_items) - self.mass.register_api_command(f"music/{api_base}/get", self.get) - self.mass.register_api_command(f"music/{api_base}/get_{self.media_type}", self.get) - self.mass.register_api_command(f"music/{api_base}/add", self.add_item_to_library) - self.mass.register_api_command(f"music/{api_base}/update", self.update_item_in_library) - self.mass.register_api_command(f"music/{api_base}/remove", self.remove_item_from_library) - self._db_add_lock = asyncio.Lock() - - async def add_item_to_library( - self, - item: ItemCls, - overwrite_existing: bool = False, - ) -> ItemCls: - """Add item to library and return the new (or updated) database item.""" - new_item = False - # check for existing item first - if library_id := await self._get_library_item_by_match(item): - # update existing item - await self._update_library_item(library_id, item, overwrite=overwrite_existing) - else: - # actually add a new item in the library db - async with self._db_add_lock: - library_id = await self._add_library_item(item) - new_item = True - # return final library_item - library_item = await self.get_library_item(library_id) - self.mass.signal_event( - EventType.MEDIA_ITEM_ADDED if new_item else EventType.MEDIA_ITEM_UPDATED, - library_item.uri, - library_item, - ) - return library_item - - async def _get_library_item_by_match(self, item: Track | ItemMapping) -> int | None: - if item.provider == "library": - return int(item.item_id) - # search by provider mappings - if isinstance(item, ItemMapping): - if cur_item := await self.get_library_item_by_prov_id(item.item_id, item.provider): - return cur_item.item_id - elif cur_item := await self.get_library_item_by_prov_mappings(item.provider_mappings): - return cur_item.item_id - if cur_item := await self.get_library_item_by_external_ids(item.external_ids): - # existing item match by external id - # Double check external IDs - if MBID exists, regards that as overriding - if compare_media_item(item, cur_item): - return cur_item.item_id - # search by (exact) name match - query = f"{self.db_table}.name = :name OR {self.db_table}.sort_name = :sort_name" - query_params = {"name": item.name, "sort_name": item.sort_name} - async for db_item in self.iter_library_items( - extra_query=query, extra_query_params=query_params - ): - if compare_media_item(db_item, item, True): - return db_item.item_id - return None - - async def update_item_in_library( - self, item_id: str | int, update: ItemCls, overwrite: bool = False - ) -> ItemCls: - """Update existing library record in the library database.""" - await self._update_library_item(item_id, update, overwrite=overwrite) - # return the updated object - library_item = await self.get_library_item(item_id) - self.mass.signal_event( - EventType.MEDIA_ITEM_UPDATED, - library_item.uri, - library_item, - ) - return library_item - - async def remove_item_from_library(self, item_id: str | int) -> None: - """Delete library record from the database.""" - db_id = int(item_id) # ensure integer - library_item = await self.get_library_item(db_id) - assert library_item, f"Item does not exist: {db_id}" - # delete item - await self.mass.music.database.delete( - self.db_table, - {"item_id": db_id}, - ) - # update provider_mappings table - await self.mass.music.database.delete( - DB_TABLE_PROVIDER_MAPPINGS, - {"media_type": self.media_type.value, "item_id": db_id}, - ) - # cleanup playlog table - await self.mass.music.database.delete( - DB_TABLE_PLAYLOG, - { - "media_type": self.media_type.value, - "item_id": db_id, - "provider": "library", - }, - ) - for prov_mapping in library_item.provider_mappings: - await self.mass.music.database.delete( - DB_TABLE_PLAYLOG, - { - "media_type": self.media_type.value, - "item_id": prov_mapping.item_id, - "provider": prov_mapping.provider_instance, - }, - ) - # NOTE: this does not delete any references to this item in other records, - # this is handled/overridden in the mediatype specific controllers - self.mass.signal_event(EventType.MEDIA_ITEM_DELETED, library_item.uri, library_item) - self.logger.debug("deleted item with id %s from database", db_id) - - async def library_count(self, favorite_only: bool = False) -> int: - """Return the total number of items in the library.""" - if favorite_only: - sql_query = f"SELECT item_id FROM {self.db_table} WHERE favorite = 1" - return await self.mass.music.database.get_count_from_query(sql_query) - return await self.mass.music.database.get_count(self.db_table) - - async def library_items( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int = 500, - offset: int = 0, - order_by: str = "sort_name", - provider: str | None = None, - extra_query: str | None = None, - extra_query_params: dict[str, Any] | None = None, - ) -> list[ItemCls]: - """Get in-database items.""" - return await self._get_library_items_by_query( - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - provider=provider, - extra_query_parts=[extra_query] if extra_query else None, - extra_query_params=extra_query_params, - ) - - async def iter_library_items( - self, - favorite: bool | None = None, - search: str | None = None, - order_by: str = "sort_name", - provider: str | None = None, - extra_query: str | None = None, - extra_query_params: dict[str, Any] | None = None, - ) -> AsyncGenerator[ItemCls, None]: - """Iterate all in-database items.""" - limit: int = 500 - offset: int = 0 - while True: - next_items = await self.library_items( - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - provider=provider, - extra_query=extra_query, - extra_query_params=extra_query_params, - ) - for item in next_items: - yield item - if len(next_items) < limit: - break - offset += limit - - async def get( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> ItemCls: - """Return (full) details for a single media item.""" - # always prefer the full library item if we have it - if library_item := await self.get_library_item_by_prov_id( - item_id, - provider_instance_id_or_domain, - ): - # schedule a refresh of the metadata on access of the item - # e.g. the item is being played or opened in the UI - self.mass.metadata.schedule_update_metadata(library_item.uri) - return library_item - # grab full details from the provider - return await self.get_provider_item( - item_id, - provider_instance_id_or_domain, - ) - - async def search( - self, - search_query: str, - provider_instance_id_or_domain: str, - limit: int = 25, - ) -> list[ItemCls]: - """Search database or provider with given query.""" - # create safe search string - search_query = search_query.replace("/", " ").replace("'", "") - if provider_instance_id_or_domain == "library": - return await self.library_items(search=search_query, limit=limit) - prov = self.mass.get_provider(provider_instance_id_or_domain) - if prov is None: - return [] - if ProviderFeature.SEARCH not in prov.supported_features: - return [] - if not prov.library_supported(self.media_type): - # assume library supported also means that this mediatype is supported - return [] - - # prefer cache items (if any) - cache_category = CacheCategory.MUSIC_SEARCH - cache_base_key = prov.lookup_key - cache_key = f"{search_query}.{limit}.{self.media_type.value}" - if ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ) is not None: - searchresult = SearchResults.from_dict(cache) - else: - # no items in cache - get listing from provider - searchresult = await prov.search( - search_query, - [self.media_type], - limit, - ) - if self.media_type == MediaType.ARTIST: - items = searchresult.artists - elif self.media_type == MediaType.ALBUM: - items = searchresult.albums - elif self.media_type == MediaType.TRACK: - items = searchresult.tracks - elif self.media_type == MediaType.PLAYLIST: - items = searchresult.playlists - else: - items = searchresult.radio - # store (serializable items) in cache - if prov.is_streaming_provider: # do not cache filesystem results - self.mass.create_task( - self.mass.cache.set( - cache_key, - searchresult.to_dict(), - expiration=86400 * 7, - category=cache_category, - base_key=cache_base_key, - ), - ) - return items - - async def get_provider_mapping(self, item: ItemCls) -> tuple[str, str]: - """Return (first) provider and item id.""" - if not getattr(item, "provider_mappings", None): - if item.provider == "library": - item = await self.get_library_item(item.item_id) - return (item.provider, item.item_id) - for prefer_unique in (True, False): - for prov_mapping in item.provider_mappings: - if not prov_mapping.available: - continue - if provider := self.mass.get_provider( - prov_mapping.provider_instance - if prefer_unique - else prov_mapping.provider_domain - ): - if prefer_unique and provider.is_streaming_provider: - continue - return (prov_mapping.provider_instance, prov_mapping.item_id) - # last resort: return just the first entry - for prov_mapping in item.provider_mappings: - return (prov_mapping.provider_domain, prov_mapping.item_id) - - return (None, None) - - async def get_library_item(self, item_id: int | str) -> ItemCls: - """Get single library item by id.""" - db_id = int(item_id) # ensure integer - extra_query = f"WHERE {self.db_table}.item_id = {item_id}" - async for db_item in self.iter_library_items(extra_query=extra_query): - return db_item - msg = f"{self.media_type.value} not found in library: {db_id}" - raise MediaNotFoundError(msg) - - async def get_library_item_by_prov_id( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> ItemCls | None: - """Get the library item for the given provider_instance.""" - assert item_id - assert provider_instance_id_or_domain - if provider_instance_id_or_domain == "library": - return await self.get_library_item(item_id) - for item in await self.get_library_items_by_prov_id( - provider_instance_id_or_domain=provider_instance_id_or_domain, - provider_item_id=item_id, - ): - return item - return None - - async def get_library_item_by_prov_mappings( - self, - provider_mappings: list[ProviderMapping], - ) -> ItemCls | None: - """Get the library item for the given provider_instance.""" - # always prefer provider instance first - for mapping in provider_mappings: - for item in await self.get_library_items_by_prov_id( - provider_instance=mapping.provider_instance, - provider_item_id=mapping.item_id, - ): - return item - # check by domain too - for mapping in provider_mappings: - for item in await self.get_library_items_by_prov_id( - provider_domain=mapping.provider_domain, - provider_item_id=mapping.item_id, - ): - return item - return None - - async def get_library_item_by_external_id( - self, external_id: str, external_id_type: ExternalID | None = None - ) -> ItemCls | None: - """Get the library item for the given external id.""" - query = f"{self.db_table}.external_ids LIKE :external_id_str" - if external_id_type: - external_id_str = f'%"{external_id_type}","{external_id}"%' - else: - external_id_str = f'%"{external_id}"%' - for item in await self._get_library_items_by_query( - extra_query_parts=[query], extra_query_params={"external_id_str": external_id_str} - ): - return item - return None - - async def get_library_item_by_external_ids( - self, external_ids: set[tuple[ExternalID, str]] - ) -> ItemCls | None: - """Get the library item for (one of) the given external ids.""" - for external_id_type, external_id in external_ids: - if match := await self.get_library_item_by_external_id(external_id, external_id_type): - return match - return None - - async def get_library_items_by_prov_id( - self, - provider_domain: str | None = None, - provider_instance: str | None = None, - provider_instance_id_or_domain: str | None = None, - provider_item_id: str | None = None, - limit: int = 500, - offset: int = 0, - ) -> list[ItemCls]: - """Fetch all records from library for given provider.""" - assert provider_instance_id_or_domain != "library" - assert provider_domain != "library" - assert provider_instance != "library" - subquery_parts: list[str] = [] - query_params: dict[str, Any] = {} - if provider_instance: - query_params = {"prov_id": provider_instance} - subquery_parts.append("provider_mappings.provider_instance = :prov_id") - elif provider_domain: - query_params = {"prov_id": provider_domain} - subquery_parts.append("provider_mappings.provider_domain = :prov_id") - else: - query_params = {"prov_id": provider_instance_id_or_domain} - subquery_parts.append( - "(provider_mappings.provider_instance = :prov_id " - "OR provider_mappings.provider_domain = :prov_id)" - ) - if provider_item_id: - subquery_parts.append("provider_mappings.provider_item_id = :item_id") - query_params["item_id"] = provider_item_id - subquery = f"SELECT item_id FROM provider_mappings WHERE {' AND '.join(subquery_parts)}" - query = f"WHERE {self.db_table}.item_id IN ({subquery})" - return await self._get_library_items_by_query( - limit=limit, offset=offset, extra_query_parts=[query], extra_query_params=query_params - ) - - async def iter_library_items_by_prov_id( - self, - provider_instance_id_or_domain: str, - provider_item_id: str | None = None, - ) -> AsyncGenerator[ItemCls, None]: - """Iterate all records from database for given provider.""" - limit: int = 500 - offset: int = 0 - while True: - next_items = await self.get_library_items_by_prov_id( - provider_instance_id_or_domain=provider_instance_id_or_domain, - provider_item_id=provider_item_id, - limit=limit, - offset=offset, - ) - for item in next_items: - yield item - if len(next_items) < limit: - break - offset += limit - - async def set_favorite(self, item_id: str | int, favorite: bool) -> None: - """Set the favorite bool on a database item.""" - db_id = int(item_id) # ensure integer - library_item = await self.get_library_item(db_id) - if library_item.favorite == favorite: - return - match = {"item_id": db_id} - await self.mass.music.database.update(self.db_table, match, {"favorite": favorite}) - library_item = await self.get_library_item(db_id) - self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) - - async def get_provider_item( - self, - item_id: str, - provider_instance_id_or_domain: str, - force_refresh: bool = False, - fallback: ItemMapping | ItemCls = None, - ) -> ItemCls: - """Return item details for the given provider item id.""" - if provider_instance_id_or_domain == "library": - return await self.get_library_item(item_id) - if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): - raise ProviderUnavailableError(f"{provider_instance_id_or_domain} is not available") - - cache_category = CacheCategory.MUSIC_PROVIDER_ITEM - cache_base_key = provider.lookup_key - cache_key = f"{self.media_type.value}.{item_id}" - if not force_refresh and ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ): - return self.item_cls.from_dict(cache) - if provider := self.mass.get_provider(provider_instance_id_or_domain): - with suppress(MediaNotFoundError): - if item := await provider.get_item(self.media_type, item_id): - await self.mass.cache.set( - cache_key, item.to_dict(), category=cache_category, base_key=cache_base_key - ) - return item - # if we reach this point all possibilities failed and the item could not be found. - # There is a possibility that the (streaming) provider changed the id of the item - # so we return the previous details (if we have any) marked as unavailable, so - # at least we have the possibility to sort out the new id through matching logic. - fallback = fallback or await self.get_library_item_by_prov_id( - item_id, provider_instance_id_or_domain - ) - if fallback and not (isinstance(fallback, ItemMapping) and self.item_cls in (Track, Album)): - # simply return the fallback item - # NOTE: we only accept ItemMapping as fallback for flat items - # so not for tracks and albums (which rely on other objects) - return fallback - # all options exhausted, we really can not find this item - msg = ( - f"{self.media_type.value}://{item_id} not " - f"found on provider {provider_instance_id_or_domain}" - ) - raise MediaNotFoundError(msg) - - async def add_provider_mapping( - self, item_id: str | int, provider_mapping: ProviderMapping - ) -> None: - """Add provider mapping to existing library item.""" - db_id = int(item_id) # ensure integer - library_item = await self.get_library_item(db_id) - # ignore if the mapping is already present - if provider_mapping in library_item.provider_mappings: - return - library_item.provider_mappings.add(provider_mapping) - await self._set_provider_mappings(db_id, library_item.provider_mappings) - - async def remove_provider_mapping( - self, item_id: str | int, provider_instance_id: str, provider_item_id: str - ) -> None: - """Remove provider mapping(s) from item.""" - db_id = int(item_id) # ensure integer - try: - library_item = await self.get_library_item(db_id) - except MediaNotFoundError: - # edge case: already deleted / race condition - return - library_item.provider_mappings = { - x - for x in library_item.provider_mappings - if x.provider_instance != provider_instance_id and x.item_id != provider_item_id - } - # update provider_mappings table - await self.mass.music.database.delete( - DB_TABLE_PROVIDER_MAPPINGS, - { - "media_type": self.media_type.value, - "item_id": db_id, - "provider_instance": provider_instance_id, - "provider_item_id": provider_item_id, - }, - ) - # cleanup playlog table - await self.mass.music.database.delete( - DB_TABLE_PLAYLOG, - { - "media_type": self.media_type.value, - "item_id": provider_item_id, - "provider": provider_instance_id, - }, - ) - if library_item.provider_mappings: - self.logger.debug( - "removed provider_mapping %s/%s from item id %s", - provider_instance_id, - provider_item_id, - db_id, - ) - self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) - else: - # remove item if it has no more providers - with suppress(AssertionError): - await self.remove_item_from_library(db_id) - - async def remove_provider_mappings(self, item_id: str | int, provider_instance_id: str) -> None: - """Remove all provider mappings from an item.""" - db_id = int(item_id) # ensure integer - try: - library_item = await self.get_library_item(db_id) - except MediaNotFoundError: - # edge case: already deleted / race condition - library_item = None - # update provider_mappings table - await self.mass.music.database.delete( - DB_TABLE_PROVIDER_MAPPINGS, - { - "media_type": self.media_type.value, - "item_id": db_id, - "provider_instance": provider_instance_id, - }, - ) - if library_item is None: - return - # update the item's provider mappings (and check if we still have any) - library_item.provider_mappings = { - x for x in library_item.provider_mappings if x.provider_instance != provider_instance_id - } - if library_item.provider_mappings: - self.logger.debug( - "removed all provider mappings for provider %s from item id %s", - provider_instance_id, - db_id, - ) - self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) - else: - # remove item if it has no more providers - with suppress(AssertionError): - await self.remove_item_from_library(db_id) - - async def dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Track]: - """Return a list of base tracks to calculate a list of dynamic tracks.""" - ref_item = await self.get(item_id, provider_instance_id_or_domain) - for prov_mapping in ref_item.provider_mappings: - prov = self.mass.get_provider(prov_mapping.provider_instance) - if prov is None: - continue - if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: - continue - return await self._get_provider_dynamic_base_tracks( - prov_mapping.item_id, - prov_mapping.provider_instance, - ) - # Fallback to the default implementation - return await self._get_dynamic_tracks(ref_item) - - @abstractmethod - async def _add_library_item( - self, - item: ItemCls, - overwrite_existing: bool = False, - ) -> int: - """Add artist to library and return the database id.""" - - @abstractmethod - async def _update_library_item( - self, item_id: str | int, update: ItemCls, overwrite: bool = False - ) -> None: - """Update existing library record in the database.""" - - async def match_providers(self, db_item: ItemCls) -> None: - """ - Try to find match on all (streaming) providers for the provided (database) item. - - This is used to link objects of different providers/qualities together. - """ - - @abstractmethod - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Track]: - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - - @abstractmethod - async def _get_dynamic_tracks(self, media_item: ItemCls, limit: int = 25) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - - async def _get_library_items_by_query( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int = 500, - offset: int = 0, - order_by: str | None = None, - provider: str | None = None, - extra_query_parts: list[str] | None = None, - extra_query_params: dict[str, Any] | None = None, - extra_join_parts: list[str] | None = None, - ) -> list[ItemCls]: - """Fetch MediaItem records from database by building the query.""" - sql_query = self.base_query - query_params = extra_query_params or {} - query_parts: list[str] = extra_query_parts or [] - join_parts: list[str] = extra_join_parts or [] - # create special performant random query - if order_by and order_by.startswith("random"): - query_parts.append( - f"{self.db_table}.item_id in " - f"(SELECT item_id FROM {self.db_table} ORDER BY RANDOM() LIMIT {limit})" - ) - # handle search - if search: - query_params["search"] = f"%{search}%" - query_parts.append(f"{self.db_table}.name LIKE :search") - # handle favorite filter - if favorite is not None: - query_parts.append(f"{self.db_table}.favorite = :favorite") - query_params["favorite"] = favorite - # handle provider filter - if provider: - join_parts.append( - f"JOIN provider_mappings ON provider_mappings.item_id = {self.db_table}.item_id " - f"AND provider_mappings.media_type = '{self.media_type.value}' " - f"AND (provider_mappings.provider_instance = '{provider}' " - f"OR provider_mappings.provider_domain = '{provider}')" - ) - # prevent duplicate where statement - query_parts = [x[5:] if x.lower().startswith("where ") else x for x in query_parts] - # concetenate all join and/or where queries - if join_parts: - sql_query += f' {" ".join(join_parts)} ' - if query_parts: - sql_query += " WHERE " + " AND ".join(query_parts) - # build final query - sql_query += f" GROUP BY {self.db_table}.item_id" - if order_by: - if sort_key := SORT_KEYS.get(order_by): - sql_query += f" ORDER BY {sort_key}" - # return dbresult parsed to media item model - return [ - self.item_cls.from_dict(self._parse_db_row(db_row)) - for db_row in await self.mass.music.database.get_rows_from_query( - sql_query, query_params, limit=limit, offset=offset - ) - ] - - async def _set_provider_mappings( - self, - item_id: str | int, - provider_mappings: Iterable[ProviderMapping], - overwrite: bool = False, - ) -> None: - """Update the provider_items table for the media item.""" - db_id = int(item_id) # ensure integer - if overwrite: - # on overwrite, clear the provider_mappings table first - # this is done for filesystem provider changing the path (and thus item_id) - await self.mass.music.database.delete( - DB_TABLE_PROVIDER_MAPPINGS, - {"media_type": self.media_type.value, "item_id": db_id}, - ) - for provider_mapping in provider_mappings: - if not provider_mapping.provider_instance: - continue - await self.mass.music.database.insert_or_replace( - DB_TABLE_PROVIDER_MAPPINGS, - { - "media_type": self.media_type.value, - "item_id": db_id, - "provider_domain": provider_mapping.provider_domain, - "provider_instance": provider_mapping.provider_instance, - "provider_item_id": provider_mapping.item_id, - "available": provider_mapping.available, - "url": provider_mapping.url, - "audio_format": serialize_to_json(provider_mapping.audio_format), - "details": provider_mapping.details, - }, - ) - - @staticmethod - def _parse_db_row(db_row: Mapping) -> dict[str, Any]: - """Parse raw db Mapping into a dict.""" - db_row_dict = dict(db_row) - db_row_dict["provider"] = "library" - db_row_dict["favorite"] = bool(db_row_dict["favorite"]) - db_row_dict["item_id"] = str(db_row_dict["item_id"]) - - for key in JSON_KEYS: - if key not in db_row_dict: - continue - if not (raw_value := db_row_dict[key]): - continue - db_row_dict[key] = json_loads(raw_value) - - # copy track_album --> album - if track_album := db_row_dict.get("track_album"): - db_row_dict["album"] = track_album - db_row_dict["disc_number"] = track_album["disc_number"] - db_row_dict["track_number"] = track_album["track_number"] - # copy album image to itemmapping single image - if images := track_album.get("images"): - db_row_dict["album"]["image"] = next( - (x for x in images if x["type"] == "thumb"), None - ) - return db_row_dict diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/server/controllers/media/playlists.py deleted file mode 100644 index e158eb27..00000000 --- a/music_assistant/server/controllers/media/playlists.py +++ /dev/null @@ -1,458 +0,0 @@ -"""Manage MediaItems of type Playlist.""" - -from __future__ import annotations - -import random -import time -from collections.abc import AsyncGenerator -from typing import Any - -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.helpers.uri import create_uri, parse_uri -from music_assistant.common.models.enums import ( - CacheCategory, - MediaType, - ProviderFeature, - ProviderType, -) -from music_assistant.common.models.errors import ( - InvalidDataError, - MediaNotFoundError, - ProviderUnavailableError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import Playlist, PlaylistTrack, Track -from music_assistant.constants import DB_TABLE_PLAYLISTS -from music_assistant.server.models.music_provider import MusicProvider - -from .base import MediaControllerBase - - -class PlaylistController(MediaControllerBase[Playlist]): - """Controller managing MediaItems of type Playlist.""" - - db_table = DB_TABLE_PLAYLISTS - media_type = MediaType.PLAYLIST - item_cls = Playlist - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - # register (extra) api handlers - api_base = self.api_base - self.mass.register_api_command(f"music/{api_base}/create_playlist", self.create_playlist) - self.mass.register_api_command("music/playlists/playlist_tracks", self.tracks) - self.mass.register_api_command( - "music/playlists/add_playlist_tracks", self.add_playlist_tracks - ) - self.mass.register_api_command( - "music/playlists/remove_playlist_tracks", self.remove_playlist_tracks - ) - - async def tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - force_refresh: bool = False, - ) -> AsyncGenerator[PlaylistTrack, None]: - """Return playlist tracks for the given provider playlist id.""" - playlist = await self.get( - item_id, - provider_instance_id_or_domain, - ) - # a playlist can only have one provider so simply pick the first one - prov_map = next(x for x in playlist.provider_mappings) - cache_checksum = playlist.cache_checksum - # playlist tracks are not stored in the db, - # we always fetched them (cached) from the provider - page = 0 - while True: - tracks = await self._get_provider_playlist_tracks( - prov_map.item_id, - prov_map.provider_instance, - cache_checksum=cache_checksum, - page=page, - force_refresh=force_refresh, - ) - if not tracks: - break - for track in tracks: - yield track - page += 1 - - async def create_playlist( - self, name: str, provider_instance_or_domain: str | None = None - ) -> Playlist: - """Create new playlist.""" - # if provider is omitted, just pick builtin provider - if provider_instance_or_domain: - provider = self.mass.get_provider(provider_instance_or_domain) - if provider is None: - raise ProviderUnavailableError - else: - provider = self.mass.get_provider("builtin") - - # create playlist on the provider - playlist = await provider.create_playlist(name) - # add the new playlist to the library - return await self.add_item_to_library(playlist, False) - - async def add_playlist_tracks(self, db_playlist_id: str | int, uris: list[str]) -> None: # noqa: PLR0915 - """Add tracks to playlist.""" - db_id = int(db_playlist_id) # ensure integer - playlist = await self.get_library_item(db_id) - if not playlist: - msg = f"Playlist with id {db_id} not found" - raise MediaNotFoundError(msg) - if not playlist.is_editable: - msg = f"Playlist {playlist.name} is not editable" - raise InvalidDataError(msg) - - # grab all existing track ids in the playlist so we can check for duplicates - playlist_prov_map = next(iter(playlist.provider_mappings)) - playlist_prov = self.mass.get_provider(playlist_prov_map.provider_instance) - if not playlist_prov or not playlist_prov.available: - msg = f"Provider {playlist_prov_map.provider_instance} is not available" - raise ProviderUnavailableError(msg) - cur_playlist_track_ids = set() - cur_playlist_track_uris = set() - async for item in self.tracks(playlist.item_id, playlist.provider): - cur_playlist_track_uris.add(item.item_id) - cur_playlist_track_uris.add(item.uri) - - # work out the track id's that need to be added - # filter out duplicates and items that not exist on the provider. - ids_to_add: set[str] = set() - for uri in uris: - # skip if item already in the playlist - if uri in cur_playlist_track_uris: - self.logger.info( - "Not adding %s to playlist %s - it already exists", uri, playlist.name - ) - continue - - # parse uri for further processing - media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) - - # skip if item already in the playlist - if item_id in cur_playlist_track_ids: - self.logger.warning( - "Not adding %s to playlist %s - it already exists", uri, playlist.name - ) - continue - - # skip non-track items - # TODO: revisit this once we support audiobooks and podcasts ? - if media_type != MediaType.TRACK: - self.logger.warning( - "Not adding %s to playlist %s - not a track", uri, playlist.name - ) - continue - - # special: the builtin provider can handle uri's from all providers (with uri as id) - if provider_instance_id_or_domain != "library" and playlist_prov.domain == "builtin": - # note: we try not to add library uri's to the builtin playlists - # so we can survive db rebuilds - ids_to_add.add(uri) - self.logger.info( - "Adding %s to playlist %s", - uri, - playlist.name, - ) - continue - - # if target playlist is an exact provider match, we can add it - if provider_instance_id_or_domain != "library": - item_prov = self.mass.get_provider(provider_instance_id_or_domain) - if not item_prov or not item_prov.available: - self.logger.warning( - "Skip adding %s to playlist: Provider %s is not available", - uri, - provider_instance_id_or_domain, - ) - continue - if item_prov.lookup_key == playlist_prov.lookup_key: - ids_to_add.add(item_id) - continue - - # ensure we have a full (library) track (including all provider mappings) - full_track = await self.mass.music.tracks.get( - item_id, - provider_instance_id_or_domain, - recursive=provider_instance_id_or_domain != "library", - ) - track_prov_domains = {x.provider_domain for x in full_track.provider_mappings} - if ( - playlist_prov.domain != "builtin" - and playlist_prov.is_streaming_provider - and playlist_prov.domain not in track_prov_domains - ): - # try to match the track to the playlist provider - full_track.provider_mappings.update( - await self.mass.music.tracks.match_provider(playlist_prov, full_track, False) - ) - - # a track can contain multiple versions on the same provider - # simply sort by quality and just add the first available version - for track_version in sorted( - full_track.provider_mappings, key=lambda x: x.quality, reverse=True - ): - if not track_version.available: - continue - if track_version.item_id in cur_playlist_track_ids: - break # already existing in the playlist - item_prov = self.mass.get_provider(track_version.provider_instance) - if not item_prov: - continue - track_version_uri = create_uri( - MediaType.TRACK, - item_prov.lookup_key, - track_version.item_id, - ) - if track_version_uri in cur_playlist_track_uris: - self.logger.warning( - "Not adding %s to playlist %s - it already exists", - full_track.name, - playlist.name, - ) - break # already existing in the playlist - if playlist_prov.domain == "builtin": - # the builtin provider can handle uri's from all providers (with uri as id) - ids_to_add.add(track_version_uri) - self.logger.info( - "Adding %s to playlist %s", - full_track.name, - playlist.name, - ) - break - if item_prov.lookup_key == playlist_prov.lookup_key: - ids_to_add.add(track_version.item_id) - self.logger.info( - "Adding %s to playlist %s", - full_track.name, - playlist.name, - ) - break - else: - self.logger.warning( - "Can't add %s to playlist %s - it is not available on provider %s", - full_track.name, - playlist.name, - playlist_prov.name, - ) - - if not ids_to_add: - return - - # actually add the tracks to the playlist on the provider - await playlist_prov.add_playlist_tracks(playlist_prov_map.item_id, list(ids_to_add)) - # invalidate cache so tracks get refreshed - playlist.cache_checksum = str(time.time()) - await self.update_item_in_library(db_playlist_id, playlist) - - async def add_playlist_track(self, db_playlist_id: str | int, track_uri: str) -> None: - """Add (single) track to playlist.""" - await self.add_playlist_tracks(db_playlist_id, [track_uri]) - - async def remove_playlist_tracks( - self, db_playlist_id: str | int, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove multiple tracks from playlist.""" - db_id = int(db_playlist_id) # ensure integer - playlist = await self.get_library_item(db_id) - if not playlist: - msg = f"Playlist with id {db_id} not found" - raise MediaNotFoundError(msg) - if not playlist.is_editable: - msg = f"Playlist {playlist.name} is not editable" - raise InvalidDataError(msg) - for prov_mapping in playlist.provider_mappings: - provider = self.mass.get_provider(prov_mapping.provider_instance) - if ProviderFeature.PLAYLIST_TRACKS_EDIT not in provider.supported_features: - self.logger.warning( - "Provider %s does not support editing playlists", - prov_mapping.provider_domain, - ) - continue - await provider.remove_playlist_tracks(prov_mapping.item_id, positions_to_remove) - # invalidate cache so tracks get refreshed - playlist.cache_checksum = str(time.time()) - await self.update_item_in_library(db_playlist_id, playlist) - - async def _add_library_item(self, item: Playlist) -> int: - """Add a new record to the database.""" - db_id = await self.mass.music.database.insert( - self.db_table, - { - "name": item.name, - "sort_name": item.sort_name, - "owner": item.owner, - "is_editable": item.is_editable, - "favorite": item.favorite, - "metadata": serialize_to_json(item.metadata), - "external_ids": serialize_to_json(item.external_ids), - "cache_checksum": item.cache_checksum, - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, item.provider_mappings) - self.logger.debug("added %s to database (id: %s)", item.name, db_id) - return db_id - - async def _update_library_item( - self, item_id: int, update: Playlist, overwrite: bool = False - ) -> None: - """Update existing record in the database.""" - db_id = int(item_id) # ensure integer - cur_item = await self.get_library_item(db_id) - metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) - cur_item.external_ids.update(update.external_ids) - await self.mass.music.database.update( - self.db_table, - {"item_id": db_id}, - { - # always prefer name/owner from updated item here - "name": update.name, - "sort_name": update.sort_name - if (overwrite or update.name != cur_item.name) - else cur_item.sort_name, - "owner": update.owner or cur_item.owner, - "is_editable": update.is_editable, - "metadata": serialize_to_json(metadata), - "external_ids": serialize_to_json( - update.external_ids if overwrite else cur_item.external_ids - ), - "cache_checksum": update.cache_checksum or cur_item.cache_checksum, - }, - ) - # update/set provider_mappings table - provider_mappings = ( - update.provider_mappings - if overwrite - else {*cur_item.provider_mappings, *update.provider_mappings} - ) - await self._set_provider_mappings(db_id, provider_mappings, overwrite) - self.logger.debug("updated %s in database: (id %s)", update.name, db_id) - - async def _get_provider_playlist_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - cache_checksum: Any = None, - page: int = 0, - force_refresh: bool = False, - ) -> list[PlaylistTrack]: - """Return playlist tracks for the given provider playlist id.""" - assert provider_instance_id_or_domain != "library" - provider: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain) - if not provider: - return [] - # prefer cache items (if any) - cache_category = CacheCategory.MUSIC_PLAYLIST_TRACKS - cache_base_key = provider.lookup_key - cache_key = f"{item_id}.{page}" - if ( - not force_refresh - and ( - cache := await self.mass.cache.get( - cache_key, - checksum=cache_checksum, - category=cache_category, - base_key=cache_base_key, - ) - ) - is not None - ): - return [PlaylistTrack.from_dict(x) for x in cache] - # no items in cache (or force_refresh) - get listing from provider - items = await provider.get_playlist_tracks(item_id, page=page) - # store (serializable items) in cache - self.mass.create_task( - self.mass.cache.set( - cache_key, - [x.to_dict() for x in items], - checksum=cache_checksum, - category=cache_category, - base_key=cache_base_key, - ) - ) - for item in items: - # if this is a complete track object, pre-cache it as - # that will save us an (expensive) lookup later - if item.image and item.artist_str and item.album and provider.domain != "builtin": - await self.mass.cache.set( - f"track.{item_id}", - item.to_dict(), - category=CacheCategory.MUSIC_PROVIDER_ITEM, - base_key=provider.lookup_key, - ) - return items - - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ): - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - assert provider_instance_id_or_domain != "library" - playlist = await self.get(item_id, provider_instance_id_or_domain) - return [ - x - async for x in self.tracks(playlist.item_id, playlist.provider) - # filter out unavailable tracks - if x.available - ] - - async def _get_dynamic_tracks( - self, - media_item: Playlist, - limit: int = 25, - ) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - # check if we have any provider that supports dynamic tracks - # TODO: query metadata provider(s) (such as lastfm?) - # to get similar tracks (or tracks from similar artists) - for prov in self.mass.get_providers(ProviderType.MUSIC): - if ProviderFeature.SIMILAR_TRACKS in prov.supported_features: - break - else: - msg = "No Music Provider found that supports requesting similar tracks." - raise UnsupportedFeaturedException(msg) - - radio_items: list[Track] = [] - radio_item_titles: set[str] = set() - playlist_tracks = [x async for x in self.tracks(media_item.item_id, media_item.provider)] - random.shuffle(playlist_tracks) - for playlist_track in playlist_tracks: - # prefer library item if available so we can use all providers - if playlist_track.provider != "library" and ( - db_item := await self.mass.music.tracks.get_library_item_by_prov_id( - playlist_track.item_id, playlist_track.provider - ) - ): - playlist_track = db_item # noqa: PLW2901 - - if not playlist_track.available: - continue - # include base item in the list - radio_items.append(playlist_track) - radio_item_titles.add(playlist_track.name) - # now try to find similar tracks - for item_prov_mapping in playlist_track.provider_mappings: - if not (prov := self.mass.get_provider(item_prov_mapping.provider_instance)): - continue - if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: - continue - # fetch some similar tracks on this provider - for similar_track in await prov.get_similar_tracks( - prov_track_id=item_prov_mapping.item_id, limit=5 - ): - if similar_track.name not in radio_item_titles: - radio_items.append(similar_track) - radio_item_titles.add(similar_track.name) - continue - if len(radio_items) >= limit: - break - # Shuffle the final items list - random.shuffle(radio_items) - return radio_items diff --git a/music_assistant/server/controllers/media/radio.py b/music_assistant/server/controllers/media/radio.py deleted file mode 100644 index b8f391d2..00000000 --- a/music_assistant/server/controllers/media/radio.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Manage MediaItems of type Radio.""" - -from __future__ import annotations - -import asyncio - -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.media_items import Radio, Track -from music_assistant.constants import DB_TABLE_RADIOS -from music_assistant.server.helpers.compare import loose_compare_strings - -from .base import MediaControllerBase - - -class RadioController(MediaControllerBase[Radio]): - """Controller managing MediaItems of type Radio.""" - - db_table = DB_TABLE_RADIOS - media_type = MediaType.RADIO - item_cls = Radio - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - # register (extra) api handlers - api_base = self.api_base - self.mass.register_api_command(f"music/{api_base}/radio_versions", self.versions) - - async def versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Radio]: - """Return all versions of a radio station we can find on all providers.""" - radio = await self.get(item_id, provider_instance_id_or_domain) - # perform a search on all provider(types) to collect all versions/variants - all_versions = { - prov_item.item_id: prov_item - for prov_items in await asyncio.gather( - *[ - self.search(radio.name, provider_domain) - for provider_domain in self.mass.music.get_unique_providers() - ] - ) - for prov_item in prov_items - if loose_compare_strings(radio.name, prov_item.name) - } - # make sure that the 'base' version is NOT included - for prov_version in radio.provider_mappings: - all_versions.pop(prov_version.item_id, None) - - # return the aggregated result - return all_versions.values() - - async def _add_library_item(self, item: Radio) -> int: - """Add a new item record to the database.""" - db_id = await self.mass.music.database.insert( - self.db_table, - { - "name": item.name, - "sort_name": item.sort_name, - "favorite": item.favorite, - "metadata": serialize_to_json(item.metadata), - "external_ids": serialize_to_json(item.external_ids), - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, item.provider_mappings) - self.logger.debug("added %s to database (id: %s)", item.name, db_id) - return db_id - - async def _update_library_item( - self, item_id: str | int, update: Radio, overwrite: bool = False - ) -> None: - """Update existing record in the database.""" - db_id = int(item_id) # ensure integer - cur_item = await self.get_library_item(db_id) - metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) - cur_item.external_ids.update(update.external_ids) - match = {"item_id": db_id} - await self.mass.music.database.update( - self.db_table, - match, - { - # always prefer name from updated item here - "name": update.name if overwrite else cur_item.name, - "sort_name": update.sort_name - if overwrite - else cur_item.sort_name or update.sort_name, - "metadata": serialize_to_json(metadata), - "external_ids": serialize_to_json( - update.external_ids if overwrite else cur_item.external_ids - ), - }, - ) - # update/set provider_mappings table - provider_mappings = ( - update.provider_mappings - if overwrite - else {*cur_item.provider_mappings, *update.provider_mappings} - ) - await self._set_provider_mappings(db_id, provider_mappings, overwrite) - self.logger.debug("updated %s in database: (id %s)", update.name, db_id) - - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - limit: int = 25, - ) -> list[Track]: - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - msg = "Dynamic tracks not supported for Radio MediaItem" - raise NotImplementedError(msg) - - async def _get_dynamic_tracks(self, media_item: Radio, limit: int = 25) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - msg = "Dynamic tracks not supported for Radio MediaItem" - raise NotImplementedError(msg) diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/server/controllers/media/tracks.py deleted file mode 100644 index 5ea9cd3d..00000000 --- a/music_assistant/server/controllers/media/tracks.py +++ /dev/null @@ -1,593 +0,0 @@ -"""Manage MediaItems of type Track.""" - -from __future__ import annotations - -import urllib.parse -from collections.abc import Iterable -from contextlib import suppress -from typing import Any - -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import MediaType, ProviderFeature -from music_assistant.common.models.errors import ( - InvalidDataError, - MediaNotFoundError, - MusicAssistantError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import ( - Album, - Artist, - ItemMapping, - ProviderMapping, - Track, - UniqueList, -) -from music_assistant.constants import ( - DB_TABLE_ALBUM_TRACKS, - DB_TABLE_ALBUMS, - DB_TABLE_TRACK_ARTISTS, - DB_TABLE_TRACKS, -) -from music_assistant.server.helpers.compare import ( - compare_artists, - compare_media_item, - compare_track, - loose_compare_strings, -) -from music_assistant.server.models.music_provider import MusicProvider - -from .base import MediaControllerBase - - -class TracksController(MediaControllerBase[Track]): - """Controller managing MediaItems of type Track.""" - - db_table = DB_TABLE_TRACKS - media_type = MediaType.TRACK - item_cls = Track - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - self.base_query = """ - SELECT - tracks.*, - (SELECT JSON_GROUP_ARRAY( - json_object( - 'item_id', provider_mappings.provider_item_id, - 'provider_domain', provider_mappings.provider_domain, - 'provider_instance', provider_mappings.provider_instance, - 'available', provider_mappings.available, - 'audio_format', json(provider_mappings.audio_format), - 'url', provider_mappings.url, - 'details', provider_mappings.details - )) FROM provider_mappings WHERE provider_mappings.item_id = tracks.item_id AND media_type = 'track') AS provider_mappings, - - (SELECT JSON_GROUP_ARRAY( - json_object( - 'item_id', artists.item_id, - 'provider', 'library', - 'name', artists.name, - 'sort_name', artists.sort_name, - 'media_type', 'artist' - )) FROM artists JOIN track_artists on track_artists.track_id = tracks.item_id WHERE artists.item_id = track_artists.artist_id) AS artists, - (SELECT - json_object( - 'item_id', albums.item_id, - 'provider', 'library', - 'name', albums.name, - 'sort_name', albums.sort_name, - 'media_type', 'album', - 'disc_number', album_tracks.disc_number, - 'track_number', album_tracks.track_number, - 'images', json_extract(albums.metadata, '$.images') - ) FROM albums WHERE albums.item_id = album_tracks.album_id) AS track_album - FROM tracks - LEFT JOIN album_tracks on album_tracks.track_id = tracks.item_id - """ # noqa: E501 - # register (extra) api handlers - api_base = self.api_base - self.mass.register_api_command(f"music/{api_base}/track_versions", self.versions) - self.mass.register_api_command(f"music/{api_base}/track_albums", self.albums) - self.mass.register_api_command(f"music/{api_base}/preview", self.get_preview_url) - - async def get( - self, - item_id: str, - provider_instance_id_or_domain: str, - recursive: bool = True, - album_uri: str | None = None, - ) -> Track: - """Return (full) details for a single media item.""" - track = await super().get( - item_id, - provider_instance_id_or_domain, - ) - if not recursive and album_uri is None: - # return early if we do not want recursive full details and no album uri is provided - return track - - # append full album details to full track item (resolve ItemMappings) - try: - if album_uri and (album := await self.mass.music.get_item_by_uri(album_uri)): - track.album = album - elif provider_instance_id_or_domain == "library": - # grab the first album this track is attached to - for album_track_row in await self.mass.music.database.get_rows( - DB_TABLE_ALBUM_TRACKS, {"track_id": int(item_id)}, limit=1 - ): - track.album = await self.mass.music.albums.get_library_item( - album_track_row["album_id"] - ) - elif isinstance(track.album, ItemMapping) or (track.album and not track.album.image): - track.album = await self.mass.music.albums.get( - track.album.item_id, track.album.provider, recursive=False - ) - except MusicAssistantError as err: - # edge case where playlist track has invalid albumdetails - self.logger.warning("Unable to fetch album details for %s - %s", track.uri, str(err)) - - if not recursive: - return track - - # append artist details to full track item (resolve ItemMappings) - track_artists = [] - for artist in track.artists: - if not isinstance(artist, ItemMapping): - track_artists.append(artist) - continue - try: - track_artists.append( - await self.mass.music.artists.get( - artist.item_id, - artist.provider, - ) - ) - except MusicAssistantError as err: - # edge case where playlist track has invalid artistdetails - self.logger.warning("Unable to fetch artist details %s - %s", artist.uri, str(err)) - track.artists = track_artists - return track - - async def library_items( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int = 500, - offset: int = 0, - order_by: str = "sort_name", - provider: str | None = None, - extra_query: str | None = None, - extra_query_params: dict[str, Any] | None = None, - ) -> list[Track]: - """Get in-database tracks.""" - extra_query_params: dict[str, Any] = extra_query_params or {} - extra_query_parts: list[str] = [extra_query] if extra_query else [] - extra_join_parts: list[str] = [] - if search and " - " in search: - # handle combined artist + title search - artist_str, title_str = search.split(" - ", 1) - search = None - extra_query_parts.append("tracks.name LIKE :search_title") - extra_query_params["search_title"] = f"%{title_str}%" - # use join with artists table to filter on artist name - extra_join_parts.append( - "JOIN track_artists ON track_artists.track_id = tracks.item_id " - "JOIN artists ON artists.item_id = track_artists.artist_id " - "AND artists.name LIKE :search_artist" - ) - extra_query_params["search_artist"] = f"%{artist_str}%" - result = await self._get_library_items_by_query( - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - provider=provider, - extra_query_parts=extra_query_parts, - extra_query_params=extra_query_params, - extra_join_parts=extra_join_parts, - ) - if search and len(result) < 25 and not offset: - # append artist items to result - extra_join_parts.append( - "JOIN track_artists ON track_artists.track_id = tracks.item_id " - "JOIN artists ON artists.item_id = track_artists.artist_id " - "AND artists.name LIKE :search_artist" - ) - extra_query_params["search_artist"] = f"%{search}%" - return result + await self._get_library_items_by_query( - favorite=favorite, - search=None, - limit=limit, - order_by=order_by, - provider=provider, - extra_query_parts=extra_query_parts, - extra_query_params=extra_query_params, - extra_join_parts=extra_join_parts, - ) - return result - - async def versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> UniqueList[Track]: - """Return all versions of a track we can find on all providers.""" - track = await self.get(item_id, provider_instance_id_or_domain) - search_query = f"{track.artist_str} - {track.name}" - result: UniqueList[Track] = UniqueList() - for provider_id in self.mass.music.get_unique_providers(): - provider = self.mass.get_provider(provider_id) - if not provider: - continue - if not provider.library_supported(MediaType.TRACK): - continue - result.extend( - prov_item - for prov_item in await self.search(search_query, provider_id) - if loose_compare_strings(track.name, prov_item.name) - and compare_artists(prov_item.artists, track.artists, any_match=True) - # make sure that the 'base' version is NOT included - and not track.provider_mappings.intersection(prov_item.provider_mappings) - ) - return result - - async def albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> UniqueList[Album]: - """Return all albums the track appears on.""" - full_track = await self.get(item_id, provider_instance_id_or_domain) - db_items = ( - await self.get_library_track_albums(full_track.item_id) - if full_track.provider == "library" - else [] - ) - # return all (unique) items from all providers - result: UniqueList[Album] = UniqueList(db_items) - if full_track.provider == "library" and in_library_only: - # return in-library items only - return result - # use search to get all items on the provider - search_query = f"{full_track.artist_str} - {full_track.name}" - # TODO: we could use musicbrainz info here to get a list of all releases known - unique_ids: set[str] = set() - for prov_item in (await self.mass.music.search(search_query, [MediaType.TRACK])).tracks: - if not loose_compare_strings(full_track.name, prov_item.name): - continue - if not prov_item.album: - continue - if not compare_artists(full_track.artists, prov_item.artists, any_match=True): - continue - unique_id = f"{prov_item.album.name}.{prov_item.album.version}" - if unique_id in unique_ids: - continue - unique_ids.add(unique_id) - # prefer db item - if db_item := await self.mass.music.albums.get_library_item_by_prov_id( - prov_item.album.item_id, prov_item.album.provider - ): - result.append(db_item) - elif not in_library_only: - result.append(prov_item.album) - return result - - async def remove_item_from_library(self, item_id: str | int) -> None: - """Delete record from the database.""" - db_id = int(item_id) # ensure integer - # delete entry(s) from albumtracks table - await self.mass.music.database.delete(DB_TABLE_ALBUM_TRACKS, {"track_id": db_id}) - # delete entry(s) from trackartists table - await self.mass.music.database.delete(DB_TABLE_TRACK_ARTISTS, {"track_id": db_id}) - # delete the track itself from db - await super().remove_item_from_library(db_id) - - async def get_preview_url(self, provider_instance_id_or_domain: str, item_id: str) -> str: - """Return url to short preview sample.""" - track = await self.get_provider_item(item_id, provider_instance_id_or_domain) - # prefer provider-provided preview - if preview := track.metadata.preview: - return preview - # fallback to a preview/sample hosted by our own webserver - enc_track_id = urllib.parse.quote(item_id) - return ( - f"{self.mass.webserver.base_url}/preview?" - f"provider={provider_instance_id_or_domain}&item_id={enc_track_id}" - ) - - async def get_library_track_albums( - self, - item_id: str | int, - ) -> list[Album]: - """Return all in-library albums for a track.""" - subquery = ( - f"SELECT album_id FROM {DB_TABLE_ALBUM_TRACKS} " - f"WHERE {DB_TABLE_ALBUM_TRACKS}.track_id = {item_id}" - ) - query = f"{DB_TABLE_ALBUMS}.item_id in ({subquery})" - return await self.mass.music.albums._get_library_items_by_query(extra_query_parts=[query]) - - async def match_providers(self, db_track: Track) -> None: - """Try to find matching track on all providers for the provided (database) track_id. - - This is used to link objects of different providers/qualities together. - """ - if db_track.provider != "library": - return # Matching only supported for database items - track_albums = await self.albums(db_track.item_id, db_track.provider) - for provider in self.mass.music.providers: - if ProviderFeature.SEARCH not in provider.supported_features: - continue - if not provider.is_streaming_provider: - # matching on unique providers is pointless as they push (all) their content to MA - continue - if not provider.library_supported(MediaType.TRACK): - continue - provider_matches = await self.match_provider( - provider, db_track, strict=True, ref_albums=track_albums - ) - for provider_mapping in provider_matches: - # 100% match, we update the db with the additional provider mapping(s) - await self.add_provider_mapping(db_track.item_id, provider_mapping) - db_track.provider_mappings.add(provider_mapping) - - async def match_provider( - self, - provider: MusicProvider, - ref_track: Track, - strict: bool = True, - ref_albums: list[Album] | None = None, - ) -> set[ProviderMapping]: - """Try to find matching track on given provider.""" - if ref_albums is None: - ref_albums = await self.albums(ref_track.item_id, ref_track.provider) - if ProviderFeature.SEARCH not in provider.supported_features: - raise UnsupportedFeaturedException("Provider does not support search") - if not provider.is_streaming_provider: - raise UnsupportedFeaturedException("Matching only possible for streaming providers") - self.logger.debug("Trying to match track %s on provider %s", ref_track.name, provider.name) - matches: set[ProviderMapping] = set() - for artist in ref_track.artists: - if matches: - break - search_str = f"{artist.name} - {ref_track.name}" - search_result = await self.search(search_str, provider.domain) - for search_result_item in search_result: - if not search_result_item.available: - continue - # do a basic compare first - if not compare_media_item(ref_track, search_result_item, strict=False): - continue - # we must fetch the full version, search results can be simplified objects - prov_track = await self.get_provider_item( - search_result_item.item_id, - search_result_item.provider, - fallback=search_result_item, - ) - if compare_track(ref_track, prov_track, strict=strict, track_albums=ref_albums): - matches.update(search_result_item.provider_mappings) - - if not matches: - self.logger.debug( - "Could not find match for Track %s on provider %s", - ref_track.name, - provider.name, - ) - return matches - - async def get_provider_similar_tracks( - self, item_id: str, provider_instance_id_or_domain: str, limit: int = 25 - ): - """Get a list of similar tracks from the provider, based on the track.""" - ref_item = await self.get(item_id, provider_instance_id_or_domain) - for prov_mapping in ref_item.provider_mappings: - prov = self.mass.get_provider(prov_mapping.provider_instance) - if prov is None: - continue - if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: - continue - # Grab similar tracks from the music provider - return await prov.get_similar_tracks(prov_track_id=prov_mapping.item_id, limit=limit) - return [] - - async def _get_provider_dynamic_base_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - ): - """Get the list of base tracks from the controller used to calculate the dynamic radio.""" - assert provider_instance_id_or_domain != "library" - return [await self.get(item_id, provider_instance_id_or_domain)] - - async def _get_dynamic_tracks( - self, - media_item: Track, - limit: int = 25, - ) -> list[Track]: - """Get dynamic list of tracks for given item, fallback/default implementation.""" - # TODO: query metadata provider(s) to get similar tracks (or tracks from similar artists) - msg = "No Music Provider found that supports requesting similar tracks." - raise UnsupportedFeaturedException(msg) - - async def _add_library_item(self, item: Track) -> int: - """Add a new item record to the database.""" - if not isinstance(item, Track): - msg = "Not a valid Track object (ItemMapping can not be added to db)" - raise InvalidDataError(msg) - if not item.artists: - msg = "Track is missing artist(s)" - raise InvalidDataError(msg) - db_id = await self.mass.music.database.insert( - self.db_table, - { - "name": item.name, - "sort_name": item.sort_name, - "version": item.version, - "duration": item.duration, - "favorite": item.favorite, - "external_ids": serialize_to_json(item.external_ids), - "metadata": serialize_to_json(item.metadata), - }, - ) - # update/set provider_mappings table - await self._set_provider_mappings(db_id, item.provider_mappings) - # set track artist(s) - await self._set_track_artists(db_id, item.artists) - # handle track album - if item.album: - await self._set_track_album( - db_id=db_id, - album=item.album, - disc_number=getattr(item, "disc_number", 0), - track_number=getattr(item, "track_number", 0), - ) - self.logger.debug("added %s to database (id: %s)", item.name, db_id) - return db_id - - async def _update_library_item( - self, item_id: str | int, update: Track, overwrite: bool = False - ) -> None: - """Update Track record in the database, merging data.""" - db_id = int(item_id) # ensure integer - cur_item = await self.get_library_item(db_id) - metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata) - cur_item.external_ids.update(update.external_ids) - await self.mass.music.database.update( - self.db_table, - {"item_id": db_id}, - { - "name": update.name if overwrite else cur_item.name, - "sort_name": update.sort_name - if overwrite - else cur_item.sort_name or update.sort_name, - "version": update.version if overwrite else cur_item.version or update.version, - "duration": update.duration if overwrite else cur_item.duration or update.duration, - "metadata": serialize_to_json(metadata), - "external_ids": serialize_to_json( - update.external_ids if overwrite else cur_item.external_ids - ), - }, - ) - # update/set provider_mappings table - provider_mappings = ( - update.provider_mappings - if overwrite - else {*cur_item.provider_mappings, *update.provider_mappings} - ) - await self._set_provider_mappings(db_id, provider_mappings, overwrite) - # set track artist(s) - artists = update.artists if overwrite else cur_item.artists + update.artists - await self._set_track_artists(db_id, artists, overwrite=overwrite) - # update/set track album - if update.album: - await self._set_track_album( - db_id=db_id, - album=update.album, - disc_number=update.disc_number or cur_item.disc_number, - track_number=update.track_number or cur_item.track_number, - overwrite=overwrite, - ) - self.logger.debug("updated %s in database: (id %s)", update.name, db_id) - - async def _set_track_album( - self, - db_id: int, - album: Album | ItemMapping, - disc_number: int, - track_number: int, - overwrite: bool = False, - ) -> None: - """ - Store Track Album info. - - A track can exist on multiple albums so we have a mapping table between - albums and tracks which stores the relation between the two and it also - stores the track and disc number of the track within an album. - For digital releases, the discnumber will be just 0 or 1. - Track number should start counting at 1. - """ - db_album: Album | ItemMapping = None - if album.provider == "library": - db_album = album - elif existing := await self.mass.music.albums.get_library_item_by_prov_id( - album.item_id, album.provider - ): - db_album = existing - - if not db_album or overwrite: - # ensure we have an actual album object - if isinstance(album, ItemMapping): - album = await self.mass.music.albums.get_provider_item( - album.item_id, album.provider, fallback=album - ) - with suppress(MediaNotFoundError, AssertionError, InvalidDataError): - db_album = await self.mass.music.albums.add_item_to_library( - album, - overwrite_existing=overwrite, - ) - if not db_album: - # this should not happen but streaming providers can be awful sometimes - self.logger.warning( - "Unable to resolve Album %s for track %s, " - "track will be added to the library without this album!", - album.uri, - db_id, - ) - return - # write (or update) record in album_tracks table - await self.mass.music.database.insert_or_replace( - DB_TABLE_ALBUM_TRACKS, - { - "track_id": db_id, - "album_id": int(db_album.item_id), - "disc_number": disc_number, - "track_number": track_number, - }, - ) - - async def _set_track_artists( - self, db_id: int, artists: Iterable[Artist | ItemMapping], overwrite: bool = False - ) -> None: - """Store Track Artists.""" - if overwrite: - # on overwrite, clear the track_artists table first - await self.mass.music.database.delete( - DB_TABLE_TRACK_ARTISTS, - { - "track_id": db_id, - }, - ) - artist_mappings: UniqueList[ItemMapping] = UniqueList() - for artist in artists: - mapping = await self._set_track_artist(db_id, artist=artist, overwrite=overwrite) - artist_mappings.append(mapping) - - async def _set_track_artist( - self, db_id: int, artist: Artist | ItemMapping, overwrite: bool = False - ) -> ItemMapping: - """Store Track Artist info.""" - db_artist: Artist | ItemMapping = None - if artist.provider == "library": - db_artist = artist - elif existing := await self.mass.music.artists.get_library_item_by_prov_id( - artist.item_id, artist.provider - ): - db_artist = existing - - if not db_artist or overwrite: - db_artist = await self.mass.music.artists.add_item_to_library( - artist, overwrite_existing=overwrite - ) - # write (or update) record in album_artists table - await self.mass.music.database.insert_or_replace( - DB_TABLE_TRACK_ARTISTS, - { - "track_id": db_id, - "artist_id": int(db_artist.item_id), - }, - ) - return ItemMapping.from_item(db_artist) diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/server/controllers/metadata.py deleted file mode 100644 index 8140784e..00000000 --- a/music_assistant/server/controllers/metadata.py +++ /dev/null @@ -1,803 +0,0 @@ -"""All logic for metadata retrieval.""" - -from __future__ import annotations - -import asyncio -import collections -import logging -import os -import random -import urllib.parse -from base64 import b64encode -from contextlib import suppress -from time import time -from typing import TYPE_CHECKING, cast -from uuid import uuid4 - -import aiofiles -from aiohttp import web - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - AlbumType, - ConfigEntryType, - ImageType, - MediaType, - ProviderFeature, - ProviderType, -) -from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError -from music_assistant.common.models.media_items import ( - Album, - Artist, - ItemMapping, - MediaItemImage, - MediaItemType, - Playlist, - Track, -) -from music_assistant.constants import ( - CONF_LANGUAGE, - DB_TABLE_ALBUMS, - DB_TABLE_ARTISTS, - DB_TABLE_PLAYLISTS, - VARIOUS_ARTISTS_MBID, - VARIOUS_ARTISTS_NAME, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.images import create_collage, get_image_thumb -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.core_controller import CoreController - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.server.models.metadata_provider import MetadataProvider - from music_assistant.server.providers.musicbrainz import MusicbrainzProvider - -LOCALES = { - "af_ZA": "African", - "ar_AE": "Arabic (United Arab Emirates)", - "ar_EG": "Arabic (Egypt)", - "ar_SA": "Saudi Arabia", - "bg_BG": "Bulgarian", - "cs_CZ": "Czech", - "zh_CN": "Chinese", - "hr_HR": "Croatian", - "da_DK": "Danish", - "de_DE": "German", - "el_GR": "Greek", - "en_AU": "English (AU)", - "en_US": "English (US)", - "en_GB": "English (UK)", - "es_ES": "Spanish", - "et_EE": "Estonian", - "fi_FI": "Finnish", - "fr_FR": "French", - "hu_HU": "Hungarian", - "is_IS": "Icelandic", - "it_IT": "Italian", - "lt_LT": "Lithuanian", - "lv_LV": "Latvian", - "jp_JP": "Japanese", - "ko_KR": "Korean", - "nl_NL": "Dutch", - "nb_NO": "Norwegian Bokmål", - "pl_PL": "Polish", - "pt_PT": "Portuguese", - "ro_RO": "Romanian", - "ru_RU": "Russian", - "sk_SK": "Slovak", - "sl_SI": "Slovenian", - "sr_RS": "Serbian", - "sv_SE": "Swedish", - "tr_TR": "Turkish", - "uk_UA": "Ukrainian", -} - -DEFAULT_LANGUAGE = "en_US" -REFRESH_INTERVAL_ARTISTS = 60 * 60 * 24 * 90 # 90 days -REFRESH_INTERVAL_ALBUMS = 60 * 60 * 24 * 90 # 90 days -REFRESH_INTERVAL_TRACKS = 60 * 60 * 24 * 90 # 90 days -REFRESH_INTERVAL_PLAYLISTS = 60 * 60 * 24 * 7 # 7 days -PERIODIC_SCAN_INTERVAL = 60 * 60 * 24 # 1 day -CONF_ENABLE_ONLINE_METADATA = "enable_online_metadata" - - -class MetaDataController(CoreController): - """Several helpers to search and store metadata for mediaitems.""" - - domain: str = "metadata" - config: CoreConfig - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - self.cache = self.mass.cache - self._pref_lang: str | None = None - self.manifest.name = "Metadata controller" - self.manifest.description = ( - "Music Assistant's core controller which handles all metadata for music." - ) - self.manifest.icon = "book-information-variant" - self._lookup_jobs: MetadataLookupQueue = MetadataLookupQueue() - self._lookup_task: asyncio.Task | None = None - self._throttler = Throttler(1, 30) - self._missing_metadata_scan_task: asyncio.Task | None = None - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - return ( - ConfigEntry( - key=CONF_LANGUAGE, - type=ConfigEntryType.STRING, - label="Preferred language", - required=False, - default_value=DEFAULT_LANGUAGE, - description="Preferred language for metadata.\n\n" - "Note that English will always be used as fallback when content " - "in your preferred language is not available.", - options=tuple(ConfigValueOption(value, key) for key, value in LOCALES.items()), - ), - ConfigEntry( - key=CONF_ENABLE_ONLINE_METADATA, - type=ConfigEntryType.BOOLEAN, - label="Enable metadata retrieval from online metadata providers", - required=False, - default_value=True, - description="Enable online metadata lookups.\n\n" - "This will allow Music Assistant to fetch additional metadata from (enabled) " - "metadata providers, such as The Audio DB and Fanart.tv.\n\n" - "Note that these online sources are only queried when no information is already " - "available from local files or the music providers and local artwork/metadata " - "will always have preference over online sources so consider metadata from online " - "sources as complementary only.\n\n" - "The retrieval of additional rich metadata is a process that is executed slowly " - "in the background to not overload these free services with requests. " - "You can speedup the process by storing the images and other metadata locally.", - ), - ) - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - self.config = config - if not self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - # silence PIL logger - logging.getLogger("PIL").setLevel(logging.WARNING) - # make sure that our directory with collage images exists - self._collage_images_dir = os.path.join(self.mass.storage_path, "collage_images") - if not await asyncio.to_thread(os.path.exists, self._collage_images_dir): - await asyncio.to_thread(os.mkdir, self._collage_images_dir) - self.mass.streams.register_dynamic_route("/imageproxy", self.handle_imageproxy) - # the lookup task is used to process metadata lookup jobs - self._lookup_task = self.mass.create_task(self._process_metadata_lookup_jobs()) - # just tun the scan for missing metadata once at startup - # TODO: allows to enable/disable this in the UI and configure interval/time - self._missing_metadata_scan_task = self.mass.create_task(self._scan_missing_metadata()) - - async def close(self) -> None: - """Handle logic on server stop.""" - if self._lookup_task and not self._lookup_task.done(): - self._lookup_task.cancel() - if self._missing_metadata_scan_task and not self._missing_metadata_scan_task.done(): - self._missing_metadata_scan_task.cancel() - self.mass.streams.unregister_dynamic_route("/imageproxy") - - @property - def providers(self) -> list[MetadataProvider]: - """Return all loaded/running MetadataProviders.""" - if TYPE_CHECKING: - return cast(list[MetadataProvider], self.mass.get_providers(ProviderType.METADATA)) - return self.mass.get_providers(ProviderType.METADATA) - - @property - def preferred_language(self) -> str: - """Return preferred language for metadata (as 2 letter language code 'en').""" - return self.locale.split("_")[0] - - @property - def locale(self) -> str: - """Return preferred language for metadata (as full locale code 'en_EN').""" - return self.mass.config.get_raw_core_config_value( - self.domain, CONF_LANGUAGE, DEFAULT_LANGUAGE - ) - - @api_command("metadata/set_default_preferred_language") - def set_default_preferred_language(self, lang: str) -> None: - """ - Set the (default) preferred language. - - Reasoning behind this is that the backend can not make a wise choice for the default, - so relies on some external source that knows better to set this info, like the frontend - or a streaming provider. - Can only be set once (by this call or the user). - """ - if self.mass.config.get_raw_core_config_value(self.domain, CONF_LANGUAGE): - return # already set - # prefer exact match - if lang in LOCALES: - self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, lang) - return - # try strict matching on either locale code or region - lang = lang.lower().replace("-", "_") - for locale_code, lang_name in LOCALES.items(): - if lang in (locale_code.lower(), lang_name.lower()): - self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, locale_code) - return - # attempt loose match on language code or region code - for lang_part in (lang[:2], lang[:-2]): - for locale_code in tuple(LOCALES): - language_code, region_code = locale_code.lower().split("_", 1) - if lang_part in (language_code, region_code): - self.mass.config.set_raw_core_config_value( - self.domain, CONF_LANGUAGE, locale_code - ) - return - # if we reach this point, we couldn't match the language - self.logger.warning("%s is not a valid language", lang) - - @api_command("metadata/update_metadata") - async def update_metadata( - self, item: str | MediaItemType, force_refresh: bool = False - ) -> MediaItemType: - """Get/update extra/enhanced metadata for/on given MediaItem.""" - if isinstance(item, str): - item = await self.mass.music.get_item_by_uri(item) - if item.provider != "library": - # this shouldn't happen but just in case. - raise RuntimeError("Metadata can only be updated for library items") - # just in case it was in the queue, prevent duplicate lookups - self._lookup_jobs.pop(item.uri) - async with self._throttler: - if item.media_type == MediaType.ARTIST: - await self._update_artist_metadata(item, force_refresh=force_refresh) - if item.media_type == MediaType.ALBUM: - await self._update_album_metadata(item, force_refresh=force_refresh) - if item.media_type == MediaType.TRACK: - await self._update_track_metadata(item, force_refresh=force_refresh) - if item.media_type == MediaType.PLAYLIST: - await self._update_playlist_metadata(item, force_refresh=force_refresh) - return item - - def schedule_update_metadata(self, uri: str) -> None: - """Schedule metadata update for given MediaItem uri.""" - if "library" not in uri: - return - with suppress(asyncio.QueueFull): - self._lookup_jobs.put_nowait(uri) - - async def get_image_data_for_item( - self, - media_item: MediaItemType, - img_type: ImageType = ImageType.THUMB, - size: int = 0, - ) -> bytes | None: - """Get image data for given MedaItem.""" - img_path = await self.get_image_url_for_item( - media_item=media_item, - img_type=img_type, - ) - if not img_path: - return None - return await self.get_thumbnail(img_path, size) - - async def get_image_url_for_item( - self, - media_item: MediaItemType, - img_type: ImageType = ImageType.THUMB, - resolve: bool = True, - ) -> str | None: - """Get url to image for given media media_item.""" - if not media_item: - return None - if isinstance(media_item, ItemMapping): - media_item = await self.mass.music.get_item_by_uri(media_item.uri) - if media_item and media_item.metadata.images: - for img in media_item.metadata.images: - if img.type != img_type: - continue - if img.remotely_accessible and not resolve: - continue - if img.remotely_accessible and resolve: - return self.get_image_url(img) - return img.path - - # retry with track's album - if media_item.media_type == MediaType.TRACK and media_item.album: - return await self.get_image_url_for_item(media_item.album, img_type, resolve) - - # try artist instead for albums - if media_item.media_type == MediaType.ALBUM and media_item.artists: - return await self.get_image_url_for_item(media_item.artists[0], img_type, resolve) - - # last resort: track artist(s) - if media_item.media_type == MediaType.TRACK and media_item.artists: - for artist in media_item.artists: - return await self.get_image_url_for_item(artist, img_type, resolve) - - return None - - def get_image_url( - self, - image: MediaItemImage, - size: int = 0, - prefer_proxy: bool = False, - image_format: str = "png", - ) -> str: - """Get (proxied) URL for MediaItemImage.""" - if not image.remotely_accessible or prefer_proxy or size: - # return imageproxy url for images that need to be resolved - # the original path is double encoded - encoded_url = urllib.parse.quote(urllib.parse.quote(image.path)) - return ( - f"{self.mass.streams.base_url}/imageproxy?path={encoded_url}" - f"&provider={image.provider}&size={size}&fmt={image_format}" - ) - return image.path - - async def get_thumbnail( - self, - path: str, - provider: str, - size: int | None = None, - base64: bool = False, - image_format: str = "png", - ) -> bytes | str: - """Get/create thumbnail image for path (image url or local path).""" - if not self.mass.get_provider(provider) and not path.startswith("http"): - raise ProviderUnavailableError - thumbnail = await get_image_thumb( - self.mass, path, size=size, provider=provider, image_format=image_format - ) - if base64: - enc_image = b64encode(thumbnail).decode() - thumbnail = f"data:image/{image_format};base64,{enc_image}" - return thumbnail - - async def handle_imageproxy(self, request: web.Request) -> web.Response: - """Handle request for image proxy.""" - path = request.query["path"] - provider = request.query.get("provider", "builtin") - if provider in ("url", "file", "http"): - # temporary for backwards compatibility - provider = "builtin" - size = int(request.query.get("size", "0")) - image_format = request.query.get("fmt", "png") - if not self.mass.get_provider(provider) and not path.startswith("http"): - return web.Response(status=404) - if "%" in path: - # assume (double) encoded url, decode it - path = urllib.parse.unquote(path) - with suppress(FileNotFoundError): - image_data = await self.get_thumbnail( - path, size=size, provider=provider, image_format=image_format - ) - # we set the cache header to 1 year (forever) - # assuming that images do not/rarely change - return web.Response( - body=image_data, - headers={"Cache-Control": "max-age=31536000", "Access-Control-Allow-Origin": "*"}, - content_type=f"image/{image_format}", - ) - return web.Response(status=404) - - async def create_collage_image( - self, - images: list[MediaItemImage], - img_path: str, - fanart: bool = False, - ) -> MediaItemImage | None: - """Create collage thumb/fanart image for (in-library) playlist.""" - if len(images) < 8 and fanart or len(images) < 3: - # require at least some images otherwise this does not make a lot of sense - return None - # limit to 50 images to prevent we're going OOM - if len(images) > 50: - images = random.sample(images, 50) - else: - random.shuffle(images) - try: - # create collage thumb from playlist tracks - # if playlist has no default image (e.g. a local playlist) - dimensions = (2500, 1750) if fanart else (1500, 1500) - img_data = await create_collage(self.mass, images, dimensions) - # always overwrite existing path - async with aiofiles.open(img_path, "wb") as _file: - await _file.write(img_data) - del img_data - return MediaItemImage( - type=ImageType.FANART if fanart else ImageType.THUMB, - path=img_path, - provider="builtin", - remotely_accessible=False, - ) - except Exception as err: - self.logger.warning( - "Error while creating playlist image: %s", - str(err), - exc_info=err if self.logger.isEnabledFor(10) else None, - ) - return None - - async def _update_artist_metadata(self, artist: Artist, force_refresh: bool = False) -> None: - """Get/update rich metadata for an artist.""" - # collect metadata from all (online) music + metadata providers - # NOTE: we only do/allow this every REFRESH_INTERVAL - needs_refresh = (time() - (artist.metadata.last_refresh or 0)) > REFRESH_INTERVAL_ARTISTS - if not (force_refresh or needs_refresh): - return - - self.logger.debug("Updating metadata for Artist %s", artist.name) - unique_keys: set[str] = set() - - # collect (local) metadata from all local providers - local_provs = get_global_cache_value("non_streaming_providers") - if TYPE_CHECKING: - local_provs = cast(set[str], local_provs) - - # ensure the item is matched to all providers - await self.mass.music.artists.match_providers(artist) - - # collect metadata from all [music] providers - # note that we sort the providers by priority so that we always - # prefer local providers over online providers - for prov_mapping in sorted( - artist.provider_mappings, key=lambda x: x.priority, reverse=True - ): - if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: - continue - if prov.lookup_key in unique_keys: - continue - if prov.lookup_key not in local_provs: - unique_keys.add(prov.lookup_key) - with suppress(MediaNotFoundError): - prov_item = await self.mass.music.artists.get_provider_item( - prov_mapping.item_id, prov_mapping.provider_instance - ) - artist.metadata.update(prov_item.metadata) - - # The musicbrainz ID is mandatory for all metadata lookups - if not artist.mbid: - # TODO: Use a global cache/proxy for the MB lookups to save on API calls - if mbid := await self._get_artist_mbid(artist): - artist.mbid = mbid - - # collect metadata from all (online)[metadata] providers - # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls - if self.config.get_value(CONF_ENABLE_ONLINE_METADATA) and artist.mbid: - for provider in self.providers: - if ProviderFeature.ARTIST_METADATA not in provider.supported_features: - continue - if metadata := await provider.get_artist_metadata(artist): - artist.metadata.update(metadata) - self.logger.debug( - "Fetched metadata for Artist %s on provider %s", - artist.name, - provider.name, - ) - # update final item in library database - # set timestamp, used to determine when this function was last called - artist.metadata.last_refresh = int(time()) - await self.mass.music.artists.update_item_in_library(artist.item_id, artist) - - async def _update_album_metadata(self, album: Album, force_refresh: bool = False) -> None: - """Get/update rich metadata for an album.""" - # collect metadata from all (online) music + metadata providers - # NOTE: we only do/allow this every REFRESH_INTERVAL - needs_refresh = (time() - (album.metadata.last_refresh or 0)) > REFRESH_INTERVAL_ALBUMS - if not (force_refresh or needs_refresh): - return - - self.logger.debug("Updating metadata for Album %s", album.name) - - # ensure the item is matched to all providers (will also get other quality versions) - await self.mass.music.albums.match_providers(album) - - # collect metadata from all [music] providers - # note that we sort the providers by priority so that we always - # prefer local providers over online providers - unique_keys: set[str] = set() - local_provs = get_global_cache_value("non_streaming_providers") - if TYPE_CHECKING: - local_provs = cast(set[str], local_provs) - for prov_mapping in sorted(album.provider_mappings, key=lambda x: x.priority, reverse=True): - if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: - continue - if prov.lookup_key in unique_keys: - continue - if prov.lookup_key not in local_provs: - unique_keys.add(prov.lookup_key) - with suppress(MediaNotFoundError): - prov_item = await self.mass.music.albums.get_provider_item( - prov_mapping.item_id, prov_mapping.provider_instance - ) - album.metadata.update(prov_item.metadata) - if album.year is None and prov_item.year: - album.year = prov_item.year - if album.album_type == AlbumType.UNKNOWN: - album.album_type = prov_item.album_type - - # collect metadata from all (online) [metadata] providers - # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls - if self.config.get_value(CONF_ENABLE_ONLINE_METADATA): - for provider in self.providers: - if ProviderFeature.ALBUM_METADATA not in provider.supported_features: - continue - if metadata := await provider.get_album_metadata(album): - album.metadata.update(metadata) - self.logger.debug( - "Fetched metadata for Album %s on provider %s", - album.name, - provider.name, - ) - # update final item in library database - # set timestamp, used to determine when this function was last called - album.metadata.last_refresh = int(time()) - await self.mass.music.albums.update_item_in_library(album.item_id, album) - - async def _update_track_metadata(self, track: Track, force_refresh: bool = False) -> None: - """Get/update rich metadata for a track.""" - # collect metadata from all (online) music + metadata providers - # NOTE: we only do/allow this every REFRESH_INTERVAL - needs_refresh = (time() - (track.metadata.last_refresh or 0)) > REFRESH_INTERVAL_TRACKS - if not (force_refresh or needs_refresh): - return - - self.logger.debug("Updating metadata for Track %s", track.name) - - # ensure the item is matched to all providers (will also get other quality versions) - await self.mass.music.tracks.match_providers(track) - - # collect metadata from all [music] providers - # note that we sort the providers by priority so that we always - # prefer local providers over online providers - unique_keys: set[str] = set() - local_provs = get_global_cache_value("non_streaming_providers") - if TYPE_CHECKING: - local_provs = cast(set[str], local_provs) - for prov_mapping in sorted(track.provider_mappings, key=lambda x: x.priority, reverse=True): - if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: - continue - if prov.lookup_key in unique_keys: - continue - unique_keys.add(prov.lookup_key) - with suppress(MediaNotFoundError): - prov_item = await self.mass.music.tracks.get_provider_item( - prov_mapping.item_id, prov_mapping.provider_instance - ) - track.metadata.update(prov_item.metadata) - - # collect metadata from all [metadata] providers - # there is only little metadata available for tracks so we only fetch metadata - # from other sources if the force flag is set - if force_refresh and self.config.get_value(CONF_ENABLE_ONLINE_METADATA): - for provider in self.providers: - if ProviderFeature.TRACK_METADATA not in provider.supported_features: - continue - if metadata := await provider.get_track_metadata(track): - track.metadata.update(metadata) - self.logger.debug( - "Fetched metadata for Track %s on provider %s", - track.name, - provider.name, - ) - # set timestamp, used to determine when this function was last called - track.metadata.last_refresh = int(time()) - # update final item in library database - await self.mass.music.tracks.update_item_in_library(track.item_id, track) - - async def _update_playlist_metadata( - self, playlist: Playlist, force_refresh: bool = False - ) -> None: - """Get/update rich metadata for a playlist.""" - # collect metadata + create collage images - # NOTE: we only do/allow this every REFRESH_INTERVAL - needs_refresh = ( - time() - (playlist.metadata.last_refresh or 0) - ) > REFRESH_INTERVAL_PLAYLISTS - if not (force_refresh or needs_refresh): - return - self.logger.debug("Updating metadata for Playlist %s", playlist.name) - playlist.metadata.genres = set() - all_playlist_tracks_images: list[MediaItemImage] = [] - playlist_genres: dict[str, int] = {} - # retrieve metadata for the playlist from the tracks (such as genres etc.) - # TODO: retrieve style/mood ? - async for track in self.mass.music.playlists.tracks(playlist.item_id, playlist.provider): - if ( - track.image - and track.image not in all_playlist_tracks_images - and ( - track.image.provider in ("url", "builtin", "http") - or self.mass.get_provider(track.image.provider) - ) - ): - all_playlist_tracks_images.append(track.image) - if track.metadata.genres: - genres = track.metadata.genres - elif track.album and isinstance(track.album, Album) and track.album.metadata.genres: - genres = track.album.metadata.genres - else: - genres = set() - for genre in genres: - if genre not in playlist_genres: - playlist_genres[genre] = 0 - playlist_genres[genre] += 1 - await asyncio.sleep(0) # yield to eventloop - - playlist_genres_filtered = {genre for genre, count in playlist_genres.items() if count > 5} - playlist_genres_filtered = list(playlist_genres_filtered)[:8] - playlist.metadata.genres.update(playlist_genres_filtered) - # create collage images - cur_images = playlist.metadata.images or [] - new_images = [] - # thumb image - thumb_image = next((x for x in cur_images if x.type == ImageType.THUMB), None) - if not thumb_image or self._collage_images_dir in thumb_image.path: - thumb_image_path = ( - thumb_image.path - if thumb_image - else os.path.join(self._collage_images_dir, f"{uuid4().hex}_thumb.jpg") - ) - if collage_thumb_image := await self.create_collage_image( - all_playlist_tracks_images, thumb_image_path - ): - new_images.append(collage_thumb_image) - elif thumb_image: - # just use old image - new_images.append(thumb_image) - # fanart image - fanart_image = next((x for x in cur_images if x.type == ImageType.FANART), None) - if not fanart_image or self._collage_images_dir in fanart_image.path: - fanart_image_path = ( - fanart_image.path - if fanart_image - else os.path.join(self._collage_images_dir, f"{uuid4().hex}_fanart.jpg") - ) - if collage_fanart_image := await self.create_collage_image( - all_playlist_tracks_images, fanart_image_path, fanart=True - ): - new_images.append(collage_fanart_image) - elif fanart_image: - # just use old image - new_images.append(fanart_image) - playlist.metadata.images = new_images - # set timestamp, used to determine when this function was last called - playlist.metadata.last_refresh = int(time()) - # update final item in library database - await self.mass.music.playlists.update_item_in_library(playlist.item_id, playlist) - - async def _get_artist_mbid(self, artist: Artist) -> str | None: - """Fetch musicbrainz id by performing search using the artist name, albums and tracks.""" - if artist.mbid: - return artist.mbid - if compare_strings(artist.name, VARIOUS_ARTISTS_NAME): - return VARIOUS_ARTISTS_MBID - - musicbrainz: MusicbrainzProvider = self.mass.get_provider("musicbrainz") - if TYPE_CHECKING: - musicbrainz = cast(MusicbrainzProvider, musicbrainz) - # first try with resource URL (e.g. streaming provider share URL) - for prov_mapping in artist.provider_mappings: - if prov_mapping.url and prov_mapping.url.startswith("http"): - if mb_artist := await musicbrainz.get_artist_details_by_resource_url( - prov_mapping.url - ): - return mb_artist.id - - # start lookup of musicbrainz id using artist name, albums and tracks - ref_albums = await self.mass.music.artists.albums( - artist.item_id, artist.provider, in_library_only=False - ) - ref_tracks = await self.mass.music.artists.tracks( - artist.item_id, artist.provider, in_library_only=False - ) - # try with (strict) ref track(s), using recording id - for ref_track in ref_tracks: - if mb_artist := await musicbrainz.get_artist_details_by_track(artist.name, ref_track): - return mb_artist.id - # try with (strict) ref album(s), using releasegroup id - for ref_album in ref_albums: - if mb_artist := await musicbrainz.get_artist_details_by_album(artist.name, ref_album): - return mb_artist.id - # last restort: track matching by name - for ref_track in ref_tracks: - if not ref_track.album: - continue - if result := await musicbrainz.search( - artistname=artist.name, - albumname=ref_track.album.name, - trackname=ref_track.name, - trackversion=ref_track.version, - ): - return result[0].id - - # lookup failed - ref_albums_str = "/".join(x.name for x in ref_albums) or "none" - ref_tracks_str = "/".join(x.name for x in ref_tracks) or "none" - self.logger.debug( - "Unable to get musicbrainz ID for artist %s\n" - " - using lookup-album(s): %s\n" - " - using lookup-track(s): %s\n", - artist.name, - ref_albums_str, - ref_tracks_str, - ) - return None - - async def _process_metadata_lookup_jobs(self) -> None: - """Task to process metadata lookup jobs.""" - while True: - item_uri = await self._lookup_jobs.get() - try: - item = await self.mass.music.get_item_by_uri(item_uri) - await self.update_metadata(item) - except Exception as err: - self.logger.error( - "Error while updating metadata for %s: %s", - item_uri, - str(err), - exc_info=err if self.logger.isEnabledFor(10) else None, - ) - - async def _scan_missing_metadata(self) -> None: - """Scanner for (missing) metadata, periodically in the background.""" - self._periodic_scan = None - # Scan for missing artist images - self.logger.debug("Start lookup for missing artist images...") - query = ( - f"json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') ISNULL " - f"AND (json_extract({DB_TABLE_ARTISTS}.metadata,'$.images') ISNULL " - f"OR json_extract({DB_TABLE_ARTISTS}.metadata,'$.images') = '[]')" - ) - for artist in await self.mass.music.artists.library_items(extra_query=query): - self.schedule_update_metadata(artist.uri) - - # Scan for missing album images - self.logger.debug("Start lookup for missing album images...") - query = ( - f"json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') ISNULL " - f"AND (json_extract({DB_TABLE_ALBUMS}.metadata,'$.images') ISNULL " - f"OR json_extract({DB_TABLE_ALBUMS}.metadata,'$.images') = '[]')" - ) - for album in await self.mass.music.albums.library_items( - limit=50, order_by="random", extra_query=query - ): - self.schedule_update_metadata(album.uri) - - # Force refresh playlist metadata every refresh interval - # this will e.g. update the playlist image and genres if the tracks have changed - timestamp = int(time() - REFRESH_INTERVAL_PLAYLISTS) - query = ( - f"json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') ISNULL " - f"OR json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') < {timestamp}" - ) - for playlist in await self.mass.music.playlists.library_items( - limit=10, order_by="random", extra_query=query - ): - self.schedule_update_metadata(playlist.uri) - - -class MetadataLookupQueue(asyncio.Queue): - """Representation of a queue for metadata lookups.""" - - def _init(self, maxlen: int = 100): - self._queue: collections.deque[str] = collections.deque(maxlen=maxlen) - - def _put(self, item: str) -> None: - if item not in self._queue: - self._queue.append(item) - - def pop(self, item: str) -> None: - """Remove item from queue.""" - self._queue.remove(item) diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py deleted file mode 100644 index 1d1f0e29..00000000 --- a/music_assistant/server/controllers/music.py +++ /dev/null @@ -1,1414 +0,0 @@ -"""MusicController: Orchestrates all data from music providers and sync to internal database.""" - -from __future__ import annotations - -import asyncio -import logging -import os -import shutil -from contextlib import suppress -from itertools import zip_longest -from math import inf -from typing import TYPE_CHECKING, Final, cast - -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.uri import parse_uri -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - CacheCategory, - ConfigEntryType, - EventType, - MediaType, - ProviderFeature, - ProviderType, -) -from music_assistant.common.models.errors import ( - InvalidProviderID, - InvalidProviderURI, - MediaNotFoundError, - MusicAssistantError, - ProviderUnavailableError, -) -from music_assistant.common.models.media_items import ( - BrowseFolder, - ItemMapping, - MediaItemType, - SearchResults, -) -from music_assistant.common.models.provider import ProviderInstance, SyncTask -from music_assistant.constants import ( - DB_TABLE_ALBUM_ARTISTS, - DB_TABLE_ALBUM_TRACKS, - DB_TABLE_ALBUMS, - DB_TABLE_ARTISTS, - DB_TABLE_LOUDNESS_MEASUREMENTS, - DB_TABLE_PLAYLISTS, - DB_TABLE_PLAYLOG, - DB_TABLE_PROVIDER_MAPPINGS, - DB_TABLE_RADIOS, - DB_TABLE_SETTINGS, - DB_TABLE_TRACK_ARTISTS, - DB_TABLE_TRACKS, - PROVIDERS_WITH_SHAREABLE_URLS, -) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.database import DatabaseConnection -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.core_controller import CoreController - -from .media.albums import AlbumsController -from .media.artists import ArtistsController -from .media.playlists import PlaylistController -from .media.radio import RadioController -from .media.tracks import TracksController - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.server.models.music_provider import MusicProvider - -CONF_RESET_DB = "reset_db" -DEFAULT_SYNC_INTERVAL = 3 * 60 # default sync interval in minutes -CONF_SYNC_INTERVAL = "sync_interval" -CONF_DELETED_PROVIDERS = "deleted_providers" -CONF_ADD_LIBRARY_ON_PLAY = "add_library_on_play" -DB_SCHEMA_VERSION: Final[int] = 9 - - -class MusicController(CoreController): - """Several helpers around the musicproviders.""" - - domain: str = "music" - database: DatabaseConnection | None = None - config: CoreConfig - - def __init__(self, *args, **kwargs) -> None: - """Initialize class.""" - super().__init__(*args, **kwargs) - self.cache = self.mass.cache - self.artists = ArtistsController(self.mass) - self.albums = AlbumsController(self.mass) - self.tracks = TracksController(self.mass) - self.radio = RadioController(self.mass) - self.playlists = PlaylistController(self.mass) - self.in_progress_syncs: list[SyncTask] = [] - self._sync_lock = asyncio.Lock() - self.manifest.name = "Music controller" - self.manifest.description = ( - "Music Assistant's core controller which manages all music from all providers." - ) - self.manifest.icon = "archive-music" - self._sync_task: asyncio.Task | None = None - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - entries = ( - ConfigEntry( - key=CONF_SYNC_INTERVAL, - type=ConfigEntryType.INTEGER, - range=(5, 720), - default_value=DEFAULT_SYNC_INTERVAL, - label="Sync interval", - description="Interval (in minutes) that a (delta) sync " - "of all providers should be performed.", - ), - ConfigEntry( - key=CONF_ADD_LIBRARY_ON_PLAY, - type=ConfigEntryType.BOOLEAN, - default_value=False, - label="Add item to the library as soon as its played", - description="Automatically add a track or radio station to " - "the library when played (if its not already in the library).", - ), - ConfigEntry( - key=CONF_RESET_DB, - type=ConfigEntryType.ACTION, - label="Reset library database", - description="This will issue a full reset of the library " - "database and trigger a full sync. Only use this option as a last resort " - "if you are seeing issues with the library database.", - category="advanced", - ), - ) - if action == CONF_RESET_DB: - await self._reset_database() - await self.mass.cache.clear() - self.start_sync() - entries = ( - *entries, - ConfigEntry( - key=CONF_RESET_DB, - type=ConfigEntryType.LABEL, - label="The database has been reset.", - ), - ) - return entries - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - self.config = config - # setup library database - await self._setup_database() - sync_interval = config.get_value(CONF_SYNC_INTERVAL) - self.logger.info("Using a sync interval of %s minutes.", sync_interval) - # make sure to finish any removal jobs - for removed_provider in self.mass.config.get_raw_core_config_value( - self.domain, CONF_DELETED_PROVIDERS, [] - ): - await self.cleanup_provider(removed_provider) - self._schedule_sync() - - async def close(self) -> None: - """Cleanup on exit.""" - if self._sync_task and not self._sync_task.done(): - self._sync_task.cancel() - await self.database.close() - - @property - def providers(self) -> list[MusicProvider]: - """Return all loaded/running MusicProviders (instances).""" - return self.mass.get_providers(ProviderType.MUSIC) - - @api_command("music/sync") - def start_sync( - self, - media_types: list[MediaType] | None = None, - providers: list[str] | None = None, - ) -> None: - """Start running the sync of (all or selected) musicproviders. - - media_types: only sync these media types. None for all. - providers: only sync these provider instances. None for all. - """ - if media_types is None: - media_types = MediaType.ALL - if providers is None: - providers = [x.instance_id for x in self.providers] - - for provider in self.providers: - if provider.instance_id not in providers: - continue - self._start_provider_sync(provider, media_types) - - @api_command("music/synctasks") - def get_running_sync_tasks(self) -> list[SyncTask]: - """Return list with providers that are currently (scheduled for) syncing.""" - return self.in_progress_syncs - - @api_command("music/search") - async def search( - self, - search_query: str, - media_types: list[MediaType] = MediaType.ALL, - limit: int = 25, - library_only: bool = False, - ) -> SearchResults: - """Perform global search for media items on all providers. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: number of items to return in the search (per type). - """ - if not media_types: - media_types = MediaType.ALL - # Check if the search query is a streaming provider public shareable URL - try: - media_type, provider_instance_id_or_domain, item_id = await parse_uri( - search_query, validate_id=True - ) - except InvalidProviderURI: - pass - except InvalidProviderID as err: - self.logger.warning("%s", str(err)) - return SearchResults() - else: - if provider_instance_id_or_domain in PROVIDERS_WITH_SHAREABLE_URLS: - try: - item = await self.get_item( - media_type=media_type, - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - except MusicAssistantError as err: - self.logger.warning("%s", str(err)) - return SearchResults() - else: - if media_type == MediaType.ARTIST: - return SearchResults(artists=[item]) - elif media_type == MediaType.ALBUM: - return SearchResults(albums=[item]) - elif media_type == MediaType.TRACK: - return SearchResults(tracks=[item]) - elif media_type == MediaType.PLAYLIST: - return SearchResults(playlists=[item]) - else: - return SearchResults() - - # include results from library + all (unique) music providers - search_providers = [] if library_only else self.get_unique_providers() - results_per_provider: list[SearchResults] = await asyncio.gather( - self.search_library(search_query, media_types, limit=limit), - *[ - self.search_provider( - search_query, - provider_instance, - media_types, - limit=limit, - ) - for provider_instance in search_providers - ], - ) - # return result from all providers while keeping index - # so the result is sorted as each provider delivered - return SearchResults( - artists=[ - item - for sublist in zip_longest(*[x.artists for x in results_per_provider]) - for item in sublist - if item is not None - ][:limit], - albums=[ - item - for sublist in zip_longest(*[x.albums for x in results_per_provider]) - for item in sublist - if item is not None - ][:limit], - tracks=[ - item - for sublist in zip_longest(*[x.tracks for x in results_per_provider]) - for item in sublist - if item is not None - ][:limit], - playlists=[ - item - for sublist in zip_longest(*[x.playlists for x in results_per_provider]) - for item in sublist - if item is not None - ][:limit], - radio=[ - item - for sublist in zip_longest(*[x.radio for x in results_per_provider]) - for item in sublist - if item is not None - ][:limit], - ) - - async def search_provider( - self, - search_query: str, - provider_instance_id_or_domain: str, - media_types: list[MediaType], - limit: int = 10, - ) -> SearchResults: - """Perform search on given provider. - - :param search_query: Search query - :param provider_instance_id_or_domain: instance_id or domain of the provider - to perform the search on. - :param media_types: A list of media_types to include. - :param limit: number of items to return in the search (per type). - """ - prov = self.mass.get_provider(provider_instance_id_or_domain) - if not prov: - return SearchResults() - if ProviderFeature.SEARCH not in prov.supported_features: - return SearchResults() - - # create safe search string - search_query = search_query.replace("/", " ").replace("'", "") - - # prefer cache items (if any) - media_types_str = ",".join(media_types) - cache_category = CacheCategory.MUSIC_SEARCH - cache_base_key = prov.lookup_key - cache_key = f"{search_query}.{limit}.{media_types_str}" - - if prov.is_streaming_provider and ( - cache := await self.mass.cache.get( - cache_key, category=cache_category, base_key=cache_base_key - ) - ): - return SearchResults.from_dict(cache) - # no items in cache - get listing from provider - result = await prov.search( - search_query, - media_types, - limit, - ) - # store (serializable items) in cache - if prov.is_streaming_provider: - self.mass.create_task( - self.mass.cache.set( - cache_key, - result.to_dict(), - expiration=86400 * 7, - category=cache_category, - base_key=cache_base_key, - ) - ) - return result - - async def search_library( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 10, - ) -> SearchResults: - """Perform search on the library. - - :param search_query: Search query - :param media_types: A list of media_types to include. - :param limit: number of items to return in the search (per type). - """ - result = SearchResults() - for media_type in media_types: - ctrl = self.get_controller(media_type) - search_results = await ctrl.search(search_query, "library", limit=limit) - if search_results: - if media_type == MediaType.ARTIST: - result.artists = search_results - elif media_type == MediaType.ALBUM: - result.albums = search_results - elif media_type == MediaType.TRACK: - result.tracks = search_results - elif media_type == MediaType.PLAYLIST: - result.playlists = search_results - elif media_type == MediaType.RADIO: - result.radio = search_results - return result - - @api_command("music/browse") - async def browse(self, path: str | None = None) -> list[MediaItemType]: - """Browse Music providers.""" - if not path or path == "root": - # root level; folder per provider - root_items: list[MediaItemType] = [] - for prov in self.providers: - if ProviderFeature.BROWSE not in prov.supported_features: - continue - root_items.append( - BrowseFolder( - item_id="root", - provider=prov.domain, - path=f"{prov.instance_id}://", - uri=f"{prov.instance_id}://", - name=prov.name, - ) - ) - return root_items - - # provider level - prepend_items: list[MediaItemType] = [] - provider_instance, sub_path = path.split("://", 1) - prov = self.mass.get_provider(provider_instance) - # handle regular provider listing, always add back folder first - if not prov or not sub_path: - prepend_items.append( - BrowseFolder(item_id="root", provider="library", path="root", name="..") - ) - if not prov: - return prepend_items - else: - back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1]) - prepend_items.append( - BrowseFolder(item_id="back", provider=provider_instance, path=back_path, name="..") - ) - # limit -1 to account for the prepended items - prov_items = await prov.browse(path=path) - return prepend_items + prov_items - - @api_command("music/recently_played_items") - async def recently_played( - self, limit: int = 10, media_types: list[MediaType] | None = None - ) -> list[MediaItemType]: - """Return a list of the last played items.""" - if media_types is None: - media_types = MediaType.ALL - media_types_str = "(" + ",".join(f'"{x}"' for x in media_types) + ")" - query = ( - f"SELECT * FROM {DB_TABLE_PLAYLOG} WHERE media_type " - f"in {media_types_str} ORDER BY timestamp DESC" - ) - db_rows = await self.mass.music.database.get_rows_from_query(query, limit=limit) - result: list[MediaItemType] = [] - for db_row in db_rows: - if db_row["provider"] not in get_global_cache_value("unique_providers", []): - continue - with suppress(MediaNotFoundError, ProviderUnavailableError): - media_type = MediaType(db_row["media_type"]) - ctrl = self.get_controller(media_type) - item = await ctrl.get( - db_row["item_id"], - db_row["provider"], - ) - result.append(item) - return result - - @api_command("music/item_by_uri") - async def get_item_by_uri(self, uri: str) -> MediaItemType: - """Fetch MediaItem by uri.""" - media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) - return await self.get_item( - media_type=media_type, - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - - @api_command("music/item") - async def get_item( - self, - media_type: MediaType, - item_id: str, - provider_instance_id_or_domain: str, - ) -> MediaItemType: - """Get single music item by id and media type.""" - if provider_instance_id_or_domain == "database": - # backwards compatibility - to remove when 2.0 stable is released - provider_instance_id_or_domain = "library" - if provider_instance_id_or_domain == "builtin": - # handle special case of 'builtin' MusicProvider which allows us to play regular url's - return await self.mass.get_provider("builtin").parse_item(item_id) - ctrl = self.get_controller(media_type) - return await ctrl.get( - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - - async def get_library_item_by_prov_id( - self, - media_type: MediaType, - item_id: str, - provider_instance_id_or_domain: str, - ) -> MediaItemType | None: - """Get single library music item by id and media type.""" - ctrl = self.get_controller(media_type) - return await ctrl.get_library_item_by_prov_id( - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - - @api_command("music/favorites/add_item") - async def add_item_to_favorites( - self, - item: str | MediaItemType, - ) -> None: - """Add an item to the favorites.""" - if isinstance(item, str): - item = await self.get_item_by_uri(item) - # ensure item is added to streaming provider library - if ( - (provider := self.mass.get_provider(item.provider)) - and provider.is_streaming_provider - and provider.library_edit_supported(item.media_type) - ): - await provider.library_add(item) - # make sure we have a full library item - # a favorite must always be in the library - full_item = await self.get_item( - item.media_type, - item.item_id, - item.provider, - ) - if full_item.provider != "library": - full_item = await self.add_item_to_library(full_item) - # set favorite in library db - ctrl = self.get_controller(item.media_type) - await ctrl.set_favorite( - full_item.item_id, - True, - ) - - @api_command("music/favorites/remove_item") - async def remove_item_from_favorites( - self, - media_type: MediaType, - library_item_id: str | int, - ) -> None: - """Remove (library) item from the favorites.""" - ctrl = self.get_controller(media_type) - await ctrl.set_favorite( - library_item_id, - False, - ) - - @api_command("music/library/remove_item") - async def remove_item_from_library( - self, media_type: MediaType, library_item_id: str | int - ) -> None: - """ - Remove item from the library. - - Destructive! Will remove the item and all dependants. - """ - ctrl = self.get_controller(media_type) - item = await ctrl.get_library_item(library_item_id) - # remove from all providers - for provider_mapping in item.provider_mappings: - if prov_controller := self.mass.get_provider(provider_mapping.provider_instance): - # we simply try to remove it on the provider library - # NOTE that the item may not be in the provider's library at all - # so we need to be a bit forgiving here - with suppress(NotImplementedError): - await prov_controller.library_remove(provider_mapping.item_id, item.media_type) - await ctrl.remove_item_from_library(library_item_id) - - @api_command("music/library/add_item") - async def add_item_to_library( - self, item: str | MediaItemType, overwrite_existing: bool = False - ) -> MediaItemType: - """Add item (uri or mediaitem) to the library.""" - if isinstance(item, str): - item = await self.get_item_by_uri(item) - if isinstance(item, ItemMapping): - item = await self.get_item( - item.media_type, - item.item_id, - item.provider, - ) - # add to provider(s) library first - for prov_mapping in item.provider_mappings: - provider = self.mass.get_provider(prov_mapping.provider_instance) - if provider.library_edit_supported(item.media_type): - prov_item = item - prov_item.provider = prov_mapping.provider_instance - prov_item.item_id = prov_mapping.item_id - await provider.library_add(prov_item) - # add (or overwrite) to library - ctrl = self.get_controller(item.media_type) - library_item = await ctrl.add_item_to_library(item, overwrite_existing) - # perform full metadata scan (and provider match) - await self.mass.metadata.update_metadata(library_item, overwrite_existing) - return library_item - - async def refresh_items(self, items: list[MediaItemType]) -> None: - """Refresh MediaItems to force retrieval of full info and matches. - - Creates background tasks to process the action. - """ - async with TaskManager(self.mass) as tg: - for media_item in items: - tg.create_task(self.refresh_item(media_item)) - - @api_command("music/refresh_item") - async def refresh_item( - self, - media_item: str | MediaItemType, - ) -> MediaItemType | None: - """Try to refresh a mediaitem by requesting it's full object or search for substitutes.""" - if isinstance(media_item, str): - # media item uri given - media_item = await self.get_item_by_uri(media_item) - - media_type = media_item.media_type - ctrl = self.get_controller(media_type) - library_id = media_item.item_id if media_item.provider == "library" else None - - available_providers = get_global_cache_value("available_providers") - if TYPE_CHECKING: - available_providers = cast(set[str], available_providers) - - # fetch the first (available) provider item - for prov_mapping in sorted( - media_item.provider_mappings, key=lambda x: x.priority, reverse=True - ): - if not self.mass.get_provider(prov_mapping.provider_instance): - # ignore unavailable providers - continue - with suppress(MediaNotFoundError): - media_item = await ctrl.get_provider_item( - prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True - ) - provider = media_item.provider - item_id = media_item.item_id - break - else: - # try to find a substitute using search - searchresult = await self.search(media_item.name, [media_item.media_type], 20) - if media_item.media_type == MediaType.ARTIST: - result = searchresult.artists - elif media_item.media_type == MediaType.ALBUM: - result = searchresult.albums - elif media_item.media_type == MediaType.TRACK: - result = searchresult.tracks - elif media_item.media_type == MediaType.PLAYLIST: - result = searchresult.playlists - else: - result = searchresult.radio - for item in result: - if item == media_item or item.provider == "library": - continue - if item.available: - provider = item.provider - item_id = item.item_id - break - else: - # raise if we didn't find a substitute - raise MediaNotFoundError(f"Could not find a substitute for {media_item.name}") - # fetch full (provider) item - media_item = await ctrl.get_provider_item(item_id, provider, force_refresh=True) - # update library item if needed (including refresh of the metadata etc.) - if library_id is None: - return media_item - library_item = await ctrl.update_item_in_library(library_id, media_item, overwrite=True) - if library_item.media_type == MediaType.ALBUM: - # update (local) album tracks - for album_track in await self.albums.tracks( - library_item.item_id, library_item.provider, True - ): - for prov_mapping in album_track.provider_mappings: - if not (prov := self.mass.get_provider(prov_mapping.provider_instance)): - continue - if prov.is_streaming_provider: - continue - with suppress(MediaNotFoundError): - prov_track = await prov.get_track(prov_mapping.item_id) - await self.mass.music.tracks.update_item_in_library( - album_track.item_id, prov_track - ) - - await self.mass.metadata.update_metadata(library_item, force_refresh=True) - return library_item - - async def set_loudness( - self, - item_id: str, - provider_instance_id_or_domain: str, - loudness: float, - album_loudness: float | None = None, - media_type: MediaType = MediaType.TRACK, - ) -> None: - """Store (EBU-R128) Integrated Loudness Measurement for a mediaitem in db.""" - if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): - return - values = { - "item_id": item_id, - "media_type": media_type.value, - "provider": provider.lookup_key, - "loudness": loudness, - } - if album_loudness is not None: - values["loudness_album"] = album_loudness - await self.database.insert_or_replace(DB_TABLE_LOUDNESS_MEASUREMENTS, values) - - async def get_loudness( - self, - item_id: str, - provider_instance_id_or_domain: str, - media_type: MediaType = MediaType.TRACK, - ) -> tuple[float, float] | 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 - db_row = await self.database.get_row( - DB_TABLE_LOUDNESS_MEASUREMENTS, - { - "item_id": item_id, - "media_type": media_type.value, - "provider": provider.lookup_key, - }, - ) - if db_row and db_row["loudness"] != inf and db_row["loudness"] != -inf: - return (db_row["loudness"], db_row["loudness_album"]) - - return None - - async def mark_item_played( - self, media_type: MediaType, item_id: str, provider_instance_id_or_domain: str - ) -> None: - """Mark item as played in playlog.""" - timestamp = utc_timestamp() - - if ( - provider_instance_id_or_domain.startswith("builtin") - and media_type != MediaType.PLAYLIST - ): - # we deliberately skip builtin provider items as those are often - # one-off items like TTS or some sound effect etc. - return - - if provider_instance_id_or_domain == "library": - prov_key = "library" - elif prov := self.mass.get_provider(provider_instance_id_or_domain): - prov_key = prov.lookup_key - else: - prov_key = provider_instance_id_or_domain - - # update generic playlog table - await self.database.insert( - DB_TABLE_PLAYLOG, - { - "item_id": item_id, - "provider": prov_key, - "media_type": media_type.value, - "timestamp": timestamp, - }, - allow_replace=True, - ) - - # also update playcount in library table - ctrl = self.get_controller(media_type) - db_item = await ctrl.get_library_item_by_prov_id(item_id, provider_instance_id_or_domain) - if ( - not db_item - and media_type in (MediaType.TRACK, MediaType.RADIO) - and self.mass.config.get_raw_core_config_value(self.domain, CONF_ADD_LIBRARY_ON_PLAY) - ): - # handle feature to add to the lib on playback - full_item = await ctrl.get(item_id, provider_instance_id_or_domain) - db_item = await ctrl.add_item_to_library(full_item) - - if db_item: - await self.database.execute( - f"UPDATE {ctrl.db_table} SET play_count = play_count + 1, " - f"last_played = {timestamp} WHERE item_id = {db_item.item_id}" - ) - await self.database.commit() - - def get_controller( - self, media_type: MediaType - ) -> ( - ArtistsController - | AlbumsController - | TracksController - | RadioController - | PlaylistController - ): - """Return controller for MediaType.""" - if media_type == MediaType.ARTIST: - return self.artists - if media_type == MediaType.ALBUM: - return self.albums - if media_type == MediaType.TRACK: - return self.tracks - if media_type == MediaType.RADIO: - return self.radio - if media_type == MediaType.PLAYLIST: - return self.playlists - return None - - def get_unique_providers(self) -> set[str]: - """ - Return all unique MusicProvider instance ids. - - This will return all filebased instances but only one instance - for streaming providers. - """ - instances = set() - domains = set() - for provider in self.providers: - if provider.domain not in domains or not provider.is_streaming_provider: - instances.add(provider.instance_id) - domains.add(provider.domain) - return instances - - def _start_provider_sync( - self, provider: ProviderInstance, media_types: tuple[MediaType, ...] - ) -> None: - """Start sync task on provider and track progress.""" - # check if we're not already running a sync task for this provider/mediatype - for sync_task in self.in_progress_syncs: - if sync_task.provider_instance != provider.instance_id: - continue - for media_type in media_types: - if media_type in sync_task.media_types: - self.logger.debug( - "Skip sync task for %s because another task is already in progress", - provider.name, - ) - return - - async def run_sync() -> None: - # Wrap the provider sync into a lock to prevent - # race conditions when multiple providers are syncing at the same time. - async with self._sync_lock: - await provider.sync_library(media_types) - # precache playlist tracks - if MediaType.PLAYLIST in media_types: - for playlist in await self.playlists.library_items(provider=provider.instance_id): - async for _ in self.playlists.tracks(playlist.item_id, playlist.provider): - pass - - # we keep track of running sync tasks - task = self.mass.create_task(run_sync()) - sync_spec = SyncTask( - provider_domain=provider.domain, - provider_instance=provider.instance_id, - media_types=media_types, - task=task, - ) - self.in_progress_syncs.append(sync_spec) - - self.mass.signal_event(EventType.SYNC_TASKS_UPDATED, data=self.in_progress_syncs) - - def on_sync_task_done(task: asyncio.Task) -> None: - self.in_progress_syncs.remove(sync_spec) - if task.cancelled(): - return - if task_err := task.exception(): - self.logger.warning( - "Sync task for %s completed with errors", - provider.name, - exc_info=task_err if self.logger.isEnabledFor(10) else None, - ) - else: - self.logger.info("Sync task for %s completed", provider.name) - self.mass.signal_event(EventType.SYNC_TASKS_UPDATED, data=self.in_progress_syncs) - # schedule db cleanup after sync - if not self.in_progress_syncs: - self.mass.create_task(self._cleanup_database()) - - task.add_done_callback(on_sync_task_done) - - async def cleanup_provider(self, provider_instance: str) -> None: - """Cleanup provider records from the database.""" - if provider_instance.startswith(("filesystem", "jellyfin", "plex", "opensubsonic")): - # removal of a local provider can become messy very fast due to the relations - # such as images pointing at the files etc. so we just reset the whole db - self.logger.warning( - "Removal of local provider detected, issuing full database reset..." - ) - await self._reset_database() - return - deleted_providers = self.mass.config.get_raw_core_config_value( - self.domain, CONF_DELETED_PROVIDERS, [] - ) - # we add the provider to this hidden config setting just to make sure that - # we can survive this over a restart to make sure that entries are cleaned up - if provider_instance not in deleted_providers: - deleted_providers.append(provider_instance) - self.mass.config.set_raw_core_config_value( - self.domain, CONF_DELETED_PROVIDERS, deleted_providers - ) - self.mass.config.save(True) - - # always clear cache when a provider is removed - await self.mass.cache.clear() - - # cleanup media items from db matched to deleted provider - self.logger.info( - "Removing provider %s from library, this can take a a while...", provider_instance - ) - errors = 0 - for ctrl in ( - # order is important here to recursively cleanup bottom up - self.mass.music.radio, - self.mass.music.playlists, - self.mass.music.tracks, - self.mass.music.albums, - self.mass.music.artists, - # run main controllers twice to rule out relations - self.mass.music.tracks, - self.mass.music.albums, - self.mass.music.artists, - ): - query = ( - f"SELECT item_id FROM {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE media_type = '{ctrl.media_type}' " - f"AND provider_instance = '{provider_instance}'" - ) - for db_row in await self.database.get_rows_from_query(query, limit=100000): - try: - await ctrl.remove_provider_mappings(db_row["item_id"], provider_instance) - except Exception as err: - # we dont want the whole removal process to stall on one item - # so in case of an unexpected error, we log and move on. - self.logger.warning( - "Error while removing %s: %s", - db_row["item_id"], - str(err), - exc_info=err if self.logger.isEnabledFor(logging.DEBUG) else None, - ) - errors += 1 - - # remove all orphaned items (not in provider mappings table anymore) - query = ( - f"SELECT item_id FROM {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE provider_instance = '{provider_instance}'" - ) - if remaining_items_count := await self.database.get_count_from_query(query): - errors += remaining_items_count - - # cleanup playlog table - await self.mass.music.database.delete( - DB_TABLE_PLAYLOG, - { - "provider": provider_instance, - }, - ) - - if errors == 0: - # cleanup successful, remove from the deleted_providers setting - self.logger.info("Provider %s removed from library", provider_instance) - deleted_providers.remove(provider_instance) - self.mass.config.set_raw_core_config_value( - self.domain, CONF_DELETED_PROVIDERS, deleted_providers - ) - else: - self.logger.warning( - "Provider %s was not not fully removed from library", provider_instance - ) - - def _schedule_sync(self) -> None: - """Schedule the periodic sync.""" - self.start_sync() - sync_interval = self.config.get_value(CONF_SYNC_INTERVAL) - # we reschedule ourselves right after execution - # NOTE: sync_interval is stored in minutes, we need seconds - self.mass.loop.call_later(sync_interval * 60, self._schedule_sync) - - async def _cleanup_database(self) -> None: - """Perform database cleanup/maintenance.""" - self.logger.debug("Performing database cleanup...") - # Remove playlog entries older than 90 days - await self.database.delete_where_query( - DB_TABLE_PLAYLOG, f"timestamp < strftime('%s','now') - {3600 * 24 * 90}" - ) - # db tables cleanup - for ctrl in (self.albums, self.artists, self.tracks, self.playlists, self.radio): - # Provider mappings where the db item is removed - query = ( - f"item_id not in (SELECT item_id from {ctrl.db_table}) " - f"AND media_type = '{ctrl.media_type}'" - ) - await self.database.delete_where_query(DB_TABLE_PROVIDER_MAPPINGS, query) - # Orphaned db items - query = ( - f"item_id not in (SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE media_type = '{ctrl.media_type}')" - ) - await self.database.delete_where_query(ctrl.db_table, query) - # Cleanup removed db items from the playlog - where_clause = ( - f"media_type = '{ctrl.media_type}' AND provider = 'library' " - f"AND item_id not in (select item_id from {ctrl.db_table})" - ) - await self.mass.music.database.delete_where_query(DB_TABLE_PLAYLOG, where_clause) - self.logger.debug("Database cleanup done") - - async def _setup_database(self) -> None: - """Initialize database.""" - db_path = os.path.join(self.mass.storage_path, "library.db") - self.database = DatabaseConnection(db_path) - await self.database.setup() - - # always create db tables if they don't exist to prevent errors trying to access them later - await self.__create_database_tables() - try: - if db_row := await self.database.get_row(DB_TABLE_SETTINGS, {"key": "version"}): - prev_version = int(db_row["value"]) - else: - prev_version = 0 - except (KeyError, ValueError): - prev_version = 0 - - if prev_version not in (0, DB_SCHEMA_VERSION): - # db version mismatch - we need to do a migration - # make a backup of db file - db_path_backup = db_path + ".backup" - await asyncio.to_thread(shutil.copyfile, db_path, db_path_backup) - - # handle db migration from previous schema(s) to this one - try: - await self.__migrate_database(prev_version) - except Exception as err: - # if the migration fails completely we reset the db - # so the user at least can have a working situation back - # a backup file is made with the previous version - self.logger.error( - "Database migration failed - starting with a fresh library database, " - "a full rescan will be performed, this can take a while!", - ) - if not isinstance(err, MusicAssistantError): - self.logger.exception(err) - - await self.database.close() - await asyncio.to_thread(os.remove, db_path) - self.database = DatabaseConnection(db_path) - await self.database.setup() - await self.mass.cache.clear() - await self.__create_database_tables() - - # store current schema version - await self.database.insert_or_replace( - DB_TABLE_SETTINGS, - {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"}, - ) - # create indexes and triggers if needed - await self.__create_database_indexes() - await self.__create_database_triggers() - # compact db - self.logger.debug("Compacting database...") - try: - await self.database.vacuum() - except Exception as err: - self.logger.warning("Database vacuum failed: %s", str(err)) - else: - self.logger.debug("Compacting database done") - - async def __migrate_database(self, prev_version: int) -> None: - """Perform a database migration.""" - # ruff: noqa: PLR0915 - self.logger.info( - "Migrating database from version %s to %s", prev_version, DB_SCHEMA_VERSION - ) - - if prev_version <= 4: - # unhandled schema version - # we do not try to handle more complex migrations - self.logger.warning( - "Database schema too old - Resetting library/database - " - "a full rescan will be performed, this can take a while!" - ) - for table in ( - DB_TABLE_TRACKS, - DB_TABLE_ALBUMS, - DB_TABLE_ARTISTS, - DB_TABLE_PLAYLISTS, - DB_TABLE_RADIOS, - DB_TABLE_ALBUM_TRACKS, - DB_TABLE_PLAYLOG, - DB_TABLE_PROVIDER_MAPPINGS, - ): - await self.database.execute(f"DROP TABLE IF EXISTS {table}") - await self.database.commit() - # recreate missing tables - await self.__create_database_tables() - return - - if prev_version <= 7: - # remove redundant artists and provider_mappings columns - for table in ( - DB_TABLE_TRACKS, - DB_TABLE_ALBUMS, - DB_TABLE_ARTISTS, - DB_TABLE_RADIOS, - DB_TABLE_PLAYLISTS, - ): - for column in ("artists", "provider_mappings"): - try: - await self.database.execute(f"ALTER TABLE {table} DROP COLUMN {column}") - except Exception as err: - if "no such column" in str(err): - continue - raise - # add cache_checksum column to playlists - try: - await self.database.execute( - f"ALTER TABLE {DB_TABLE_PLAYLISTS} ADD COLUMN cache_checksum TEXT DEFAULT ''" - ) - except Exception as err: - if "duplicate column" not in str(err): - raise - - if prev_version <= 8: - # migrate track_loudness --> loudness_measurements - async for db_row in self.database.iter_items("track_loudness"): - if db_row["integrated"] == inf or db_row["integrated"] == -inf: - continue - if db_row["provider"] in ("radiobrowser", "tunein"): - continue - await self.database.insert_or_replace( - DB_TABLE_LOUDNESS_MEASUREMENTS, - { - "item_id": db_row["item_id"], - "media_type": "track", - "provider": db_row["provider"], - "loudness": db_row["integrated"], - }, - ) - await self.database.execute("DROP TABLE IF EXISTS track_loudness") - - # save changes - await self.database.commit() - - # always clear the cache after a db migration - await self.mass.cache.clear() - - async def _reset_database(self) -> None: - """Reset the database.""" - await self.close() - db_path = os.path.join(self.mass.storage_path, "library.db") - await asyncio.to_thread(os.remove, db_path) - await self._setup_database() - - async def __create_database_tables(self) -> None: - """Create database tables.""" - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_SETTINGS}( - [key] TEXT PRIMARY KEY, - [value] TEXT, - [type] TEXT - );""" - ) - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_PLAYLOG}( - [id] INTEGER PRIMARY KEY AUTOINCREMENT, - [item_id] TEXT NOT NULL, - [provider] TEXT NOT NULL, - [media_type] TEXT NOT NULL DEFAULT 'track', - [timestamp] INTEGER DEFAULT 0, - UNIQUE(item_id, provider, media_type));""" - ) - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUMS}( - [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, - [name] TEXT NOT NULL, - [sort_name] TEXT NOT NULL, - [version] TEXT, - [album_type] TEXT NOT NULL, - [year] INTEGER, - [favorite] BOOLEAN DEFAULT 0, - [metadata] json NOT NULL, - [external_ids] json NOT NULL, - [play_count] INTEGER DEFAULT 0, - [last_played] INTEGER DEFAULT 0, - [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), - [timestamp_modified] INTEGER - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_ARTISTS}( - [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, - [name] TEXT NOT NULL, - [sort_name] TEXT NOT NULL, - [favorite] BOOLEAN DEFAULT 0, - [metadata] json NOT NULL, - [external_ids] json NOT NULL, - [play_count] INTEGER DEFAULT 0, - [last_played] INTEGER DEFAULT 0, - [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), - [timestamp_modified] INTEGER - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_TRACKS}( - [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, - [name] TEXT NOT NULL, - [sort_name] TEXT NOT NULL, - [version] TEXT, - [duration] INTEGER, - [favorite] BOOLEAN DEFAULT 0, - [metadata] json NOT NULL, - [external_ids] json NOT NULL, - [play_count] INTEGER DEFAULT 0, - [last_played] INTEGER DEFAULT 0, - [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), - [timestamp_modified] INTEGER - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_PLAYLISTS}( - [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, - [name] TEXT NOT NULL, - [sort_name] TEXT NOT NULL, - [owner] TEXT NOT NULL, - [is_editable] BOOLEAN NOT NULL, - [cache_checksum] TEXT DEFAULT '', - [favorite] BOOLEAN DEFAULT 0, - [metadata] json NOT NULL, - [external_ids] json NOT NULL, - [play_count] INTEGER DEFAULT 0, - [last_played] INTEGER DEFAULT 0, - [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), - [timestamp_modified] INTEGER - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_RADIOS}( - [item_id] INTEGER PRIMARY KEY AUTOINCREMENT, - [name] TEXT NOT NULL, - [sort_name] TEXT NOT NULL, - [favorite] BOOLEAN DEFAULT 0, - [metadata] json NOT NULL, - [external_ids] json NOT NULL, - [play_count] INTEGER DEFAULT 0, - [last_played] INTEGER DEFAULT 0, - [timestamp_added] INTEGER DEFAULT (cast(strftime('%s','now') as int)), - [timestamp_modified] INTEGER - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUM_TRACKS}( - [id] INTEGER PRIMARY KEY AUTOINCREMENT, - [track_id] INTEGER NOT NULL, - [album_id] INTEGER NOT NULL, - [disc_number] INTEGER NOT NULL, - [track_number] INTEGER NOT NULL, - FOREIGN KEY([track_id]) REFERENCES [tracks]([item_id]), - FOREIGN KEY([album_id]) REFERENCES [albums]([item_id]), - UNIQUE(track_id, album_id) - );""" - ) - await self.database.execute( - f""" - CREATE TABLE IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}( - [media_type] TEXT NOT NULL, - [item_id] INTEGER NOT NULL, - [provider_domain] TEXT NOT NULL, - [provider_instance] TEXT NOT NULL, - [provider_item_id] TEXT NOT NULL, - [available] BOOLEAN DEFAULT 1, - [url] text, - [audio_format] json, - [details] TEXT, - UNIQUE(media_type, provider_instance, provider_item_id) - );""" - ) - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}( - [track_id] INTEGER NOT NULL, - [artist_id] INTEGER NOT NULL, - FOREIGN KEY([track_id]) REFERENCES [tracks]([item_id]), - FOREIGN KEY([artist_id]) REFERENCES [artists]([item_id]), - UNIQUE(track_id, artist_id) - );""" - ) - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}( - [album_id] INTEGER NOT NULL, - [artist_id] INTEGER NOT NULL, - FOREIGN KEY([album_id]) REFERENCES [albums]([item_id]), - FOREIGN KEY([artist_id]) REFERENCES [artists]([item_id]), - UNIQUE(album_id, artist_id) - );""" - ) - - await self.database.execute( - f"""CREATE TABLE IF NOT EXISTS {DB_TABLE_LOUDNESS_MEASUREMENTS}( - [id] INTEGER PRIMARY KEY AUTOINCREMENT, - [media_type] TEXT NOT NULL, - [item_id] TEXT NOT NULL, - [provider] TEXT NOT NULL, - [loudness] REAL, - [loudness_album] REAL, - UNIQUE(media_type,item_id,provider));""" - ) - - await self.database.commit() - - async def __create_database_indexes(self) -> None: - """Create database indexes.""" - for db_table in ( - DB_TABLE_ARTISTS, - DB_TABLE_ALBUMS, - DB_TABLE_TRACKS, - DB_TABLE_PLAYLISTS, - DB_TABLE_RADIOS, - ): - # index on favorite column - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_favorite_idx on {db_table}(favorite);" - ) - # index on name - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_name_idx on {db_table}(name);" - ) - # index on name (without case sensitivity) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_name_nocase_idx " - f"ON {db_table}(name COLLATE NOCASE);" - ) - # index on sort_name - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_sort_name_idx on {db_table}(sort_name);" - ) - # index on sort_name (without case sensitivity) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_sort_name_nocase_idx " - f"ON {db_table}(sort_name COLLATE NOCASE);" - ) - # index on external_ids - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_external_ids_idx " - f"ON {db_table}(external_ids);" - ) - # index on timestamp_added - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_timestamp_added_idx " - f"on {db_table}(timestamp_added);" - ) - # index on play_count - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_play_count_idx " - f"on {db_table}(play_count);" - ) - # index on last_played - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {db_table}_last_played_idx " - f"on {db_table}(last_played);" - ) - - # indexes on provider_mappings table - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_media_type_item_id_idx " - f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,item_id);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_provider_domain_idx " - f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_domain,provider_item_id);" - ) - await self.database.execute( - f"CREATE UNIQUE INDEX IF NOT EXISTS {DB_TABLE_PROVIDER_MAPPINGS}_provider_instance_idx " - f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_instance,provider_item_id);" - ) - await self.database.execute( - "CREATE INDEX IF NOT EXISTS " - f"{DB_TABLE_PROVIDER_MAPPINGS}_media_type_provider_instance_idx " - f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_instance);" - ) - await self.database.execute( - "CREATE INDEX IF NOT EXISTS " - f"{DB_TABLE_PROVIDER_MAPPINGS}_media_type_provider_domain_idx " - f"on {DB_TABLE_PROVIDER_MAPPINGS}(media_type,provider_domain);" - ) - - # indexes on track_artists table - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}_track_id_idx " - f"on {DB_TABLE_TRACK_ARTISTS}(track_id);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_TRACK_ARTISTS}_artist_id_idx " - f"on {DB_TABLE_TRACK_ARTISTS}(artist_id);" - ) - # indexes on album_artists table - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}_album_id_idx " - f"on {DB_TABLE_ALBUM_ARTISTS}(album_id);" - ) - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_ALBUM_ARTISTS}_artist_id_idx " - f"on {DB_TABLE_ALBUM_ARTISTS}(artist_id);" - ) - # index on loudness measurements table - await self.database.execute( - f"CREATE INDEX IF NOT EXISTS {DB_TABLE_LOUDNESS_MEASUREMENTS}_idx " - f"on {DB_TABLE_LOUDNESS_MEASUREMENTS}(media_type,item_id,provider);" - ) - await self.database.commit() - - async def __create_database_triggers(self) -> None: - """Create database triggers.""" - # triggers to auto update timestamps - for db_table in ("artists", "albums", "tracks", "playlists", "radios"): - await self.database.execute( - f""" - CREATE TRIGGER IF NOT EXISTS update_{db_table}_timestamp - AFTER UPDATE ON {db_table} FOR EACH ROW - WHEN NEW.timestamp_modified <= OLD.timestamp_modified - BEGIN - UPDATE {db_table} set timestamp_modified=cast(strftime('%s','now') as int) - WHERE item_id=OLD.item_id; - END; - """ - ) - await self.database.commit() diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py deleted file mode 100644 index 68eb7941..00000000 --- a/music_assistant/server/controllers/player_queues.py +++ /dev/null @@ -1,1558 +0,0 @@ -""" -MusicAssistant Player Queues Controller. - -Handles all logic to PLAY Media Items, provided by Music Providers to supported players. - -It is loosely coupled to the MusicAssistant Music Controller and Player Controller. -A Music Assistant Player always has a PlayerQueue associated with it -which holds the queue items and state. - -The PlayerQueue is in that case the active source of the player, -but it can also be something else, hence the loose coupling. -""" - -from __future__ import annotations - -import asyncio -import random -import time -from typing import TYPE_CHECKING, Any, TypedDict - -from music_assistant.common.helpers.util import get_changed_keys -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - CacheCategory, - ConfigEntryType, - EventType, - MediaType, - PlayerState, - ProviderFeature, - QueueOption, - RepeatMode, -) -from music_assistant.common.models.errors import ( - InvalidCommand, - MediaNotFoundError, - MusicAssistantError, - PlayerUnavailableError, - QueueEmpty, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import ( - AudioFormat, - MediaItemType, - Playlist, - media_from_dict, -) -from music_assistant.common.models.player import PlayerMedia -from music_assistant.common.models.player_queue import PlayerQueue -from music_assistant.common.models.queue_item import QueueItem -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import CONF_CROSSFADE, CONF_FLOW_MODE, MASS_LOGO_ONLINE -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.audio import get_stream_details -from music_assistant.server.helpers.throttle_retry import BYPASS_THROTTLER -from music_assistant.server.models.core_controller import CoreController - -if TYPE_CHECKING: - from collections.abc import Iterator - - from music_assistant.common.models.media_items import Album, Artist, Track - from music_assistant.common.models.player import Player - - -CONF_DEFAULT_ENQUEUE_SELECT_ARTIST = "default_enqueue_select_artist" -CONF_DEFAULT_ENQUEUE_SELECT_ALBUM = "default_enqueue_select_album" - -ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE = "all_tracks" -ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE = "all_tracks" - -CONF_DEFAULT_ENQUEUE_OPTION_ARTIST = "default_enqueue_option_artist" -CONF_DEFAULT_ENQUEUE_OPTION_ALBUM = "default_enqueue_option_album" -CONF_DEFAULT_ENQUEUE_OPTION_TRACK = "default_enqueue_option_track" -CONF_DEFAULT_ENQUEUE_OPTION_RADIO = "default_enqueue_option_radio" -CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST = "default_enqueue_option_playlist" -RADIO_TRACK_MAX_DURATION_SECS = 20 * 60 # 20 minutes - - -class CompareState(TypedDict): - """Simple object where we store the (previous) state of a queue. - - Used for compare actions. - """ - - queue_id: str - state: PlayerState - current_index: int | None - elapsed_time: int - stream_title: str | None - content_type: str | None - - -class PlayerQueuesController(CoreController): - """Controller holding all logic to enqueue music for players.""" - - domain: str = "player_queues" - - def __init__(self, *args, **kwargs) -> None: - """Initialize core controller.""" - super().__init__(*args, **kwargs) - self._queues: dict[str, PlayerQueue] = {} - self._queue_items: dict[str, list[QueueItem]] = {} - self._prev_states: dict[str, CompareState] = {} - self.manifest.name = "Player Queues controller" - self.manifest.description = ( - "Music Assistant's core controller which manages the queues for all players." - ) - self.manifest.icon = "playlist-music" - - async def close(self) -> None: - """Cleanup on exit.""" - # stop all playback - for queue in self.all(): - if queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): - continue - await self.stop(queue.queue_id) - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - enqueue_options = tuple(ConfigValueOption(x.name, x.value) for x in QueueOption) - return ( - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, - type=ConfigEntryType.STRING, - default_value=ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, - label="Items to select when you play a (in-library) artist.", - options=( - ConfigValueOption( - title="Only in-library tracks", - value="library_tracks", - ), - ConfigValueOption( - title="All tracks from all albums in the library", - value="library_album_tracks", - ), - ConfigValueOption( - title="All (top) tracks from (all) streaming provider(s)", - value="all_tracks", - ), - ConfigValueOption( - title="All tracks from all albums from (all) streaming provider(s)", - value="all_album_tracks", - ), - ), - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, - type=ConfigEntryType.STRING, - default_value=ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, - label="Items to select when you play a (in-library) album.", - options=( - ConfigValueOption( - title="Only in-library tracks", - value="library_tracks", - ), - ConfigValueOption( - title="All tracks for album on (streaming) provider", - value="all_tracks", - ), - ), - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_OPTION_ARTIST, - type=ConfigEntryType.STRING, - default_value=QueueOption.REPLACE.value, - label="Default enqueue option for Artist item(s).", - options=enqueue_options, - description="Define the default enqueue action for this mediatype.", - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_OPTION_ALBUM, - type=ConfigEntryType.STRING, - default_value=QueueOption.REPLACE.value, - label="Default enqueue option for Album item(s).", - options=enqueue_options, - description="Define the default enqueue action for this mediatype.", - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_OPTION_TRACK, - type=ConfigEntryType.STRING, - default_value=QueueOption.PLAY.value, - label="Default enqueue option for Track item(s).", - options=enqueue_options, - description="Define the default enqueue action for this mediatype.", - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_OPTION_RADIO, - type=ConfigEntryType.STRING, - default_value=QueueOption.REPLACE.value, - label="Default enqueue option for Radio item(s).", - options=enqueue_options, - description="Define the default enqueue action for this mediatype.", - ), - ConfigEntry( - key=CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST, - type=ConfigEntryType.STRING, - default_value=QueueOption.REPLACE.value, - label="Default enqueue option for Playlist item(s).", - options=enqueue_options, - description="Define the default enqueue action for this mediatype.", - ), - ) - - def __iter__(self) -> Iterator[PlayerQueue]: - """Iterate over (available) players.""" - return iter(self._queues.values()) - - @api_command("player_queues/all") - def all(self) -> tuple[PlayerQueue, ...]: - """Return all registered PlayerQueues.""" - return tuple(self._queues.values()) - - @api_command("player_queues/get") - def get(self, queue_id: str) -> PlayerQueue | None: - """Return PlayerQueue by queue_id or None if not found.""" - return self._queues.get(queue_id) - - @api_command("player_queues/items") - def items(self, queue_id: str, limit: int = 500, offset: int = 0) -> list[QueueItem]: - """Return all QueueItems for given PlayerQueue.""" - if queue_id not in self._queue_items: - return [] - - return self._queue_items[queue_id][offset : offset + limit] - - @api_command("player_queues/get_active_queue") - def get_active_queue(self, player_id: str) -> PlayerQueue: - """Return the current active/synced queue for a player.""" - if player := self.mass.players.get(player_id): - # account for player that is synced (sync child) - if player.synced_to and player.synced_to != player.player_id: - return self.get_active_queue(player.synced_to) - # handle active group player - if player.active_group and player.active_group != player.player_id: - return self.get_active_queue(player.active_group) - # active_source may be filled with other queue id - return self.get(player.active_source) or self.get(player_id) - return self.get(player_id) - - # Queue commands - - @api_command("player_queues/shuffle") - def set_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None: - """Configure shuffle setting on the the queue.""" - queue = self._queues[queue_id] - if queue.shuffle_enabled == shuffle_enabled: - return # no change - queue.shuffle_enabled = shuffle_enabled - queue_items = self._queue_items[queue_id] - cur_index = queue.index_in_buffer or queue.current_index - if cur_index is not None: - next_index = cur_index + 1 - next_items = queue_items[next_index:] - else: - next_items = [] - next_index = 0 - if not shuffle_enabled: - # shuffle disabled, try to restore original sort order of the remaining items - next_items.sort(key=lambda x: x.sort_index, reverse=False) - self.load( - queue_id=queue_id, - queue_items=next_items, - insert_at_index=next_index, - keep_remaining=False, - shuffle=shuffle_enabled, - ) - - @api_command("player_queues/dont_stop_the_music") - def set_dont_stop_the_music(self, queue_id: str, dont_stop_the_music_enabled: bool) -> None: - """Configure Don't stop the music setting on the queue.""" - providers_available_with_similar_tracks = any( - ProviderFeature.SIMILAR_TRACKS in provider.supported_features - for provider in self.mass.music.providers - ) - if dont_stop_the_music_enabled and not providers_available_with_similar_tracks: - raise UnsupportedFeaturedException( - "Don't stop the music is not supported by any of the available music providers" - ) - queue = self._queues[queue_id] - queue.dont_stop_the_music_enabled = dont_stop_the_music_enabled - self.signal_update(queue_id=queue_id) - # if this happens to be the last track in the queue, fill the radio source - if ( - queue.dont_stop_the_music_enabled - and queue.enqueued_media_items - and queue.current_index is not None - and (queue.items - queue.current_index) <= 1 - ): - queue.radio_source = queue.enqueued_media_items - task_id = f"fill_radio_tracks_{queue_id}" - self.mass.call_later(5, self._fill_radio_tracks, queue_id, task_id=task_id) - - @api_command("player_queues/repeat") - def set_repeat(self, queue_id: str, repeat_mode: RepeatMode) -> None: - """Configure repeat setting on the the queue.""" - queue = self._queues[queue_id] - if queue.repeat_mode == repeat_mode: - return # no change - queue.repeat_mode = repeat_mode - self.signal_update(queue_id) - - @api_command("player_queues/play_media") - async def play_media( - self, - queue_id: str, - media: MediaItemType | list[MediaItemType] | str | list[str], - option: QueueOption | None = None, - radio_mode: bool = False, - start_item: str | None = None, - ) -> None: - """Play media item(s) on the given queue. - - - media: Media that should be played (MediaItem(s) or uri's). - - queue_opt: Which enqueue mode to use. - - radio_mode: Enable radio mode for the given item(s). - - start_item: Optional item to start the playlist or album from. - """ - # ruff: noqa: PLR0915,PLR0912 - # we use a contextvar to bypass the throttler for this asyncio task/context - # this makes sure that playback has priority over other requests that may be - # happening in the background - BYPASS_THROTTLER.set(True) - queue = self._queues[queue_id] - # always fetch the underlying player so we can raise early if its not available - queue_player = self.mass.players.get(queue_id, True) - if queue_player.announcement_in_progress: - self.logger.warning("Ignore queue command: An announcement is in progress") - return - - # a single item or list of items may be provided - if not isinstance(media, list): - media = [media] - - # clear queue first if it was finished - if queue.current_index and queue.current_index >= (len(self._queue_items[queue_id]) - 1): - queue.current_index = None - self._queue_items[queue_id] = [] - # clear queue if needed - if option == QueueOption.REPLACE: - self.clear(queue_id) - # Clear the 'enqueued media item' list when a new queue is requested - if option not in (QueueOption.ADD, QueueOption.NEXT): - queue.enqueued_media_items.clear() - - tracks: list[MediaItemType] = [] - radio_source: list[MediaItemType] = [] - first_track_seen: bool = False - for item in media: - try: - # parse provided uri into a MA MediaItem or Basic QueueItem from URL - if isinstance(item, str): - media_item = await self.mass.music.get_item_by_uri(item) - elif isinstance(item, dict): - media_item = media_from_dict(item) - else: - media_item = item - - # Save requested media item to play on the queue so we can use it as a source - # for Don't stop the music. Use FIFO list to keep track of the last 10 played items - if media_item.media_type in ( - MediaType.TRACK, - MediaType.ALBUM, - MediaType.PLAYLIST, - MediaType.ARTIST, - ): - queue.enqueued_media_items.append(media_item) - if len(queue.enqueued_media_items) > 10: - queue.enqueued_media_items.pop(0) - - # handle default enqueue option if needed - if option is None: - option = QueueOption( - await self.mass.config.get_core_config_value( - self.domain, - f"default_enqueue_option_{media_item.media_type.value}", - ) - ) - if option == QueueOption.REPLACE: - self.clear(queue_id) - - # collect tracks to play - if radio_mode: - radio_source.append(media_item) - elif media_item.media_type == MediaType.PLAYLIST: - tracks += await self.get_playlist_tracks(media_item, start_item) - self.mass.create_task( - self.mass.music.mark_item_played( - media_item.media_type, media_item.item_id, media_item.provider - ) - ) - elif media_item.media_type == MediaType.ARTIST: - tracks += await self.get_artist_tracks(media_item) - self.mass.create_task( - self.mass.music.mark_item_played( - media_item.media_type, media_item.item_id, media_item.provider - ) - ) - elif media_item.media_type == MediaType.ALBUM: - tracks += await self.get_album_tracks(media_item, start_item) - self.mass.create_task( - self.mass.music.mark_item_played( - media_item.media_type, media_item.item_id, media_item.provider - ) - ) - else: - # single track or radio item - tracks += [media_item] - - except MusicAssistantError as err: - # invalid MA uri or item not found error - self.logger.warning("Skipping %s: %s", item, str(err)) - - # overwrite or append radio source items - if option not in (QueueOption.ADD, QueueOption.NEXT): - queue.radio_source = radio_source - else: - queue.radio_source += radio_source - # Use collected media items to calculate the radio if radio mode is on - if radio_mode: - tracks = await self._get_radio_tracks(queue_id=queue_id, is_initial_radio_mode=True) - - # only add valid/available items - queue_items = [QueueItem.from_media_item(queue_id, x) for x in tracks if x and x.available] - - if not queue_items: - if first_track_seen: - # edge case: playlist with only one track - return - raise MediaNotFoundError("No playable items found") - - # load the items into the queue - if queue.state in (PlayerState.PLAYING, PlayerState.PAUSED): - cur_index = queue.index_in_buffer or 0 - else: - cur_index = queue.current_index or 0 - insert_at_index = cur_index + 1 if self._queue_items.get(queue_id) else 0 - # Radio modes are already shuffled in a pattern we would like to keep. - shuffle = queue.shuffle_enabled and len(queue_items) > 1 and not radio_mode - - # handle replace: clear all items and replace with the new items - if option == QueueOption.REPLACE: - self.load( - queue_id, - queue_items=queue_items, - keep_remaining=False, - keep_played=False, - shuffle=shuffle, - ) - await self.play_index(queue_id, 0) - return - # handle next: add item(s) in the index next to the playing/loaded/buffered index - if option == QueueOption.NEXT: - self.load( - queue_id, - queue_items=queue_items, - insert_at_index=insert_at_index, - shuffle=shuffle, - ) - return - if option == QueueOption.REPLACE_NEXT: - self.load( - queue_id, - queue_items=queue_items, - insert_at_index=insert_at_index, - keep_remaining=False, - shuffle=shuffle, - ) - return - # handle play: replace current loaded/playing index with new item(s) - if option == QueueOption.PLAY: - self.load( - queue_id, - queue_items=queue_items, - insert_at_index=insert_at_index, - shuffle=shuffle, - ) - next_index = min(insert_at_index, len(self._queue_items[queue_id]) - 1) - await self.play_index(queue_id, next_index) - return - # handle add: add/append item(s) to the remaining queue items - if option == QueueOption.ADD: - self.load( - queue_id=queue_id, - queue_items=queue_items, - insert_at_index=insert_at_index - if queue.shuffle_enabled - else len(self._queue_items[queue_id]), - shuffle=queue.shuffle_enabled, - ) - # handle edgecase, queue is empty and items are only added (not played) - # mark first item as new index - if queue.current_index is None: - queue.current_index = 0 - queue.current_item = self.get_item(queue_id, 0) - queue.items = len(queue_items) - self.signal_update(queue_id) - - @api_command("player_queues/move_item") - def move_item(self, queue_id: str, queue_item_id: str, pos_shift: int = 1) -> None: - """ - Move queue item x up/down the queue. - - - queue_id: id of the queue to process this request. - - queue_item_id: the item_id of the queueitem that needs to be moved. - - pos_shift: move item x positions down if positive value - - pos_shift: move item x positions up if negative value - - pos_shift: move item to top of queue as next item if 0. - """ - queue = self._queues[queue_id] - item_index = self.index_by_id(queue_id, queue_item_id) - if item_index <= queue.index_in_buffer: - msg = f"{item_index} is already played/buffered" - raise IndexError(msg) - - queue_items = self._queue_items[queue_id] - queue_items = queue_items.copy() - - if pos_shift == 0 and queue.state == PlayerState.PLAYING: - new_index = (queue.current_index or 0) + 1 - elif pos_shift == 0: - new_index = queue.current_index or 0 - else: - new_index = item_index + pos_shift - if (new_index < (queue.current_index or 0)) or (new_index > len(queue_items)): - return - # move the item in the list - queue_items.insert(new_index, queue_items.pop(item_index)) - self.update_items(queue_id, queue_items) - - @api_command("player_queues/delete_item") - def delete_item(self, queue_id: str, item_id_or_index: int | str) -> None: - """Delete item (by id or index) from the queue.""" - if isinstance(item_id_or_index, str): - item_index = self.index_by_id(queue_id, item_id_or_index) - else: - item_index = item_id_or_index - queue = self._queues[queue_id] - if item_index <= queue.index_in_buffer: - # ignore request if track already loaded in the buffer - # the frontend should guard so this is just in case - self.logger.warning("delete requested for item already loaded in buffer") - return - queue_items = self._queue_items[queue_id] - queue_items.pop(item_index) - self.update_items(queue_id, queue_items) - - @api_command("player_queues/clear") - def clear(self, queue_id: str) -> None: - """Clear all items in the queue.""" - queue = self._queues[queue_id] - queue.radio_source = [] - if queue.state != PlayerState.IDLE: - self.mass.create_task(self.stop(queue_id)) - queue.current_index = None - queue.current_item = None - queue.elapsed_time = 0 - queue.index_in_buffer = None - self.update_items(queue_id, []) - - @api_command("player_queues/stop") - async def stop(self, queue_id: str) -> None: - """ - Handle STOP command for given queue. - - - queue_id: queue_id of the playerqueue to handle the command. - """ - if (queue := self.get(queue_id)) and queue.active: - queue.resume_pos = queue.corrected_elapsed_time - # forward the actual command to the player provider - if player_provider := self.mass.players.get_player_provider(queue.queue_id): - await player_provider.cmd_stop(queue_id) - - @api_command("player_queues/play") - async def play(self, queue_id: str) -> None: - """ - Handle PLAY command for given queue. - - - queue_id: queue_id of the playerqueue to handle the command. - """ - queue_player: Player = self.mass.players.get(queue_id, True) - if ( - (queue := self._queues.get(queue_id)) - and queue.active - and queue_player.state == PlayerState.PAUSED - ): - # forward the actual play/unpause command to the player provider - if player_provider := self.mass.players.get_player_provider(queue.queue_id): - await player_provider.cmd_play(queue_id) - return - # player is not paused, perform resume instead - await self.resume(queue_id) - - @api_command("player_queues/pause") - async def pause(self, queue_id: str) -> None: - """Handle PAUSE command for given queue. - - - queue_id: queue_id of the playerqueue to handle the command. - """ - if queue := self._queues.get(queue_id): - queue.resume_pos = queue.corrected_elapsed_time - # forward the actual command to the player controller - await self.mass.players.cmd_pause(queue_id) - - @api_command("player_queues/play_pause") - async def play_pause(self, queue_id: str) -> None: - """Toggle play/pause on given playerqueue. - - - queue_id: queue_id of the queue to handle the command. - """ - if (queue := self._queues.get(queue_id)) and queue.state == PlayerState.PLAYING: - await self.pause(queue_id) - return - await self.play(queue_id) - - @api_command("player_queues/next") - async def next(self, queue_id: str) -> None: - """Handle NEXT TRACK command for given queue. - - - queue_id: queue_id of the queue to handle the command. - """ - if (queue := self.get(queue_id)) is None or not queue.active: - # TODO: forward to underlying player if not active - return - idx = self._queues[queue_id].current_index - while True: - try: - if (next_index := self._get_next_index(queue_id, idx, True)) is not None: - await self.play_index(queue_id, next_index, debounce=True) - break - except MediaNotFoundError: - self.logger.warning( - "Failed to fetch next track for queue %s - trying next item", queue.display_name - ) - idx += 1 - - @api_command("player_queues/previous") - async def previous(self, queue_id: str) -> None: - """Handle PREVIOUS TRACK command for given queue. - - - queue_id: queue_id of the queue to handle the command. - """ - if (queue := self.get(queue_id)) is None or not queue.active: - # TODO: forward to underlying player if not active - return - current_index = self._queues[queue_id].current_index - if current_index is None: - return - await self.play_index(queue_id, max(current_index - 1, 0), debounce=True) - - @api_command("player_queues/skip") - async def skip(self, queue_id: str, seconds: int = 10) -> None: - """Handle SKIP command for given queue. - - - queue_id: queue_id of the queue to handle the command. - - seconds: number of seconds to skip in track. Use negative value to skip back. - """ - if (queue := self.get(queue_id)) is None or not queue.active: - # TODO: forward to underlying player if not active - return - await self.seek(queue_id, self._queues[queue_id].elapsed_time + seconds) - - @api_command("player_queues/seek") - async def seek(self, queue_id: str, position: int = 10) -> None: - """Handle SEEK command for given queue. - - - queue_id: queue_id of the queue to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - if not (queue := self.get(queue_id)): - return - queue_player: Player = self.mass.players.get(queue_id, True) - if not queue.current_item: - raise InvalidCommand(f"Queue {queue_player.display_name} has no item(s) loaded.") - if ( - queue.current_item.media_item.media_type != MediaType.TRACK - or not queue.current_item.duration - ): - raise InvalidCommand("Can not seek on non track items.") - position = max(0, int(position)) - if position > queue.current_item.duration: - raise InvalidCommand("Can not seek outside of duration range.") - await self.play_index(queue_id, queue.current_index, seek_position=position) - - @api_command("player_queues/resume") - async def resume(self, queue_id: str, fade_in: bool | None = None) -> None: - """Handle RESUME command for given queue. - - - queue_id: queue_id of the queue to handle the command. - """ - queue = self._queues[queue_id] - queue_items = self._queue_items[queue_id] - resume_item = queue.current_item - if queue.state == PlayerState.PLAYING: - # resume requested while already playing, - # use current position as resume position - resume_pos = queue.corrected_elapsed_time - else: - resume_pos = queue.resume_pos - - if not resume_item and queue.current_index is not None and len(queue_items) > 0: - resume_item = self.get_item(queue_id, queue.current_index) - resume_pos = 0 - elif not resume_item and queue.current_index is None and len(queue_items) > 0: - # items available in queue but no previous track, start at 0 - resume_item = self.get_item(queue_id, 0) - 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 not queue_player.powered: - 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 - resume_pos = 0 - await self.play_index(queue_id, resume_item.queue_item_id, resume_pos, fade_in) - else: - msg = f"Resume queue requested but queue {queue.display_name} is empty" - raise QueueEmpty(msg) - - @api_command("player_queues/play_index") - async def play_index( - self, - queue_id: str, - index: int | str, - seek_position: int = 0, - fade_in: bool = False, - debounce: bool = False, - ) -> None: - """Play item at index (or item_id) X in queue.""" - queue = self._queues[queue_id] - queue.resume_pos = 0 - if isinstance(index, str): - index = self.index_by_id(queue_id, index) - queue_item = self.get_item(queue_id, index) - if queue_item is None: - msg = f"Unknown index/id: {index}" - raise FileNotFoundError(msg) - queue.current_index = index - queue.index_in_buffer = index - queue.flow_mode_stream_log = [] - queue.flow_mode = await self.mass.config.get_player_config_value(queue_id, CONF_FLOW_MODE) - next_index = self._get_next_index(queue_id, index, allow_repeat=False) - queue.current_item = queue_item - queue.next_track_enqueued = None - self.signal_update(queue_id) - - # work out if we are playing an album and if we should prefer album loudness - if ( - next_index is not None - and (next_item := self.get_item(queue_id, next_index)) - and ( - queue_item.media_item - and hasattr(queue_item.media_item, "album") - and hasattr(next_item.media_item, "album") - and queue_item.media_item.album - and next_item.media_item - and next_item.media_item.album - and queue_item.media_item.album.item_id == next_item.media_item.album.item_id - ) - ): - prefer_album_loudness = True - else: - prefer_album_loudness = False - - # get streamdetails - do this here to catch unavailable items early - queue_item.streamdetails = await get_stream_details( - self.mass, - queue_item, - seek_position=seek_position, - fade_in=fade_in, - prefer_album_loudness=prefer_album_loudness, - ) - - # allow stripping silence from the end of the track if crossfade is enabled - # this will allow for smoother crossfades - if await self.mass.config.get_player_config_value(queue_id, CONF_CROSSFADE): - queue_item.streamdetails.strip_silence_end = True - # send play_media request to player - # NOTE that we debounce this a bit to account for someone hitting the next button - # like a madman. This will prevent the player from being overloaded with requests. - self.mass.call_later( - 1 if debounce else 0.1, - self.mass.players.play_media, - player_id=queue_id, - # transform into PlayerMedia to send to the actual player implementation - media=self.player_media_from_queue_item(queue_item, queue.flow_mode), - task_id=f"play_media_{queue_id}", - ) - self.signal_update(queue_id) - - @api_command("player_queues/transfer") - async def transfer_queue( - self, - source_queue_id: str, - target_queue_id: str, - auto_play: bool | None = None, - ) -> None: - """Transfer queue to another queue.""" - if not (source_queue := self.get(source_queue_id)): - raise PlayerUnavailableError("Queue {source_queue_id} is not available") - if not (target_queue := self.get(target_queue_id)): - raise PlayerUnavailableError("Queue {target_queue_id} is not available") - if auto_play is None: - auto_play = source_queue.state == PlayerState.PLAYING - - target_player = self.mass.players.get(target_queue_id) - if target_player.active_group or target_player.synced_to: - # edge case: the user wants to move playback from the group as a whole, to a single - # player in the group or it is grouped and the command targeted at the single player. - # We need to dissolve the group first. - await self.mass.players.cmd_power( - target_player.active_group or target_player.synced_to, False - ) - await asyncio.sleep(3) - - source_items = self._queue_items[source_queue_id] - target_queue.repeat_mode = source_queue.repeat_mode - target_queue.shuffle_enabled = source_queue.shuffle_enabled - target_queue.dont_stop_the_music_enabled = source_queue.dont_stop_the_music_enabled - target_queue.radio_source = source_queue.radio_source - target_queue.resume_pos = source_queue.elapsed_time - target_queue.current_index = source_queue.current_index - if source_queue.current_item: - target_queue.current_item = source_queue.current_item - target_queue.current_item.queue_id = target_queue_id - self.clear(source_queue_id) - - self.load(target_queue_id, source_items, keep_remaining=False, keep_played=False) - for item in source_items: - item.queue_id = target_queue_id - self.update_items(target_queue_id, source_items) - if auto_play: - await self.resume(target_queue_id) - - # Interaction with player - - async def on_player_register(self, player: Player) -> None: - """Register PlayerQueue for given player/queue id.""" - queue_id = player.player_id - queue = None - # try to restore previous state - if prev_state := await self.mass.cache.get( - "state", category=CacheCategory.PLAYER_QUEUE_STATE, base_key=queue_id - ): - try: - queue = PlayerQueue.from_cache(prev_state) - prev_items = await self.mass.cache.get( - "items", - default=[], - category=CacheCategory.PLAYER_QUEUE_STATE, - base_key=queue_id, - ) - queue_items = [QueueItem.from_cache(x) for x in prev_items] - except Exception as err: - self.logger.warning( - "Failed to restore the queue(items) for %s - %s", - player.display_name, - str(err), - ) - if queue is None: - queue = PlayerQueue( - queue_id=queue_id, - active=False, - display_name=player.display_name, - available=player.available, - dont_stop_the_music_enabled=False, - items=0, - ) - queue_items = [] - - self._queues[queue_id] = queue - self._queue_items[queue_id] = queue_items - # always call update to calculate state etc - self.on_player_update(player, {}) - self.mass.signal_event(EventType.QUEUE_ADDED, object_id=queue_id, data=queue) - - def on_player_update( - self, - player: Player, - changed_values: dict[str, tuple[Any, Any]], - ) -> None: - """ - Call when a PlayerQueue needs to be updated (e.g. when player updates). - - NOTE: This is called every second if the player is playing. - """ - if player.player_id not in self._queues: - # race condition - return - if player.announcement_in_progress: - # do nothing while the announcement is in progress - return - queue_id = player.player_id - player = self.mass.players.get(queue_id) - queue = self._queues[queue_id] - - # basic properties - queue.display_name = player.display_name - queue.available = player.available - queue.items = len(self._queue_items[queue_id]) - # determine if this queue is currently active for this player - queue.active = player.powered and player.active_source == queue.queue_id - if not queue.active: - # return early if the queue is not active - queue.state = PlayerState.IDLE - if prev_state := self._prev_states.pop(queue_id, None): - self.signal_update(queue_id) - return - # update current item from player report - if queue.flow_mode: - # flow mode active, calculate current item - queue.current_index, queue.elapsed_time = self._get_flow_queue_stream_index( - queue, player - ) - queue.elapsed_time_last_updated = time.time() - else: - # queue is active and player has one of our tracks loaded, update state - if item_id := self._parse_player_current_item_id(queue_id, player): - queue.current_index = self.index_by_id(queue_id, item_id) - if player.state in (PlayerState.PLAYING, PlayerState.PAUSED): - queue.elapsed_time = int(player.corrected_elapsed_time or 0) - queue.elapsed_time_last_updated = player.elapsed_time_last_updated or 0 - - # only update these attributes if the queue is active - # and has an item loaded so we are able to resume it - queue.state = player.state or PlayerState.IDLE - queue.current_item = self.get_item(queue_id, queue.current_index) - queue.next_item = ( - self.get_item(queue_id, queue.next_track_enqueued) - if queue.next_track_enqueued - else self._get_next_item(queue_id, queue.current_index) - ) - - # correct elapsed time when seeking - if ( - queue.current_item - and queue.current_item.streamdetails - and queue.current_item.streamdetails.seek_position - and player.state in (PlayerState.PLAYING, PlayerState.PAUSED) - and not queue.flow_mode - ): - queue.elapsed_time += queue.current_item.streamdetails.seek_position - - # enqueue next track if needed - if ( - queue.state == PlayerState.PLAYING - and queue.next_item is not None - and not queue.next_track_enqueued - and queue.corrected_elapsed_time > 2 - ): - self._check_enqueue_next(queue) - - # basic throttle: do not send state changed events if queue did not actually change - prev_state = self._prev_states.get( - queue_id, - CompareState( - queue_id=queue_id, - state=PlayerState.IDLE, - current_index=None, - elapsed_time=0, - stream_title=None, - ), - ) - new_state = CompareState( - queue_id=queue_id, - state=queue.state, - current_index=queue.current_index, - elapsed_time=queue.elapsed_time, - stream_title=queue.current_item.streamdetails.stream_title - if queue.current_item and queue.current_item.streamdetails - else None, - content_type=queue.current_item.streamdetails.audio_format.output_format_str - if queue.current_item and queue.current_item.streamdetails - else None, - ) - changed_keys = get_changed_keys(prev_state, new_state) - # return early if nothing changed - if len(changed_keys) == 0: - return - - # do not send full updates if only time was updated - if changed_keys == {"elapsed_time"}: - self.mass.signal_event( - EventType.QUEUE_TIME_UPDATED, - object_id=queue_id, - data=queue.elapsed_time, - ) - self._prev_states[queue_id] = new_state - return - - # signal update and store state - self.signal_update(queue_id) - self._prev_states[queue_id] = new_state - - # detect change in current index to report that a item has been played - end_of_queue_reached = ( - prev_state["state"] == PlayerState.PLAYING - and new_state["state"] == PlayerState.IDLE - and queue.current_item is not None - and queue.next_item is None - ) - if ( - prev_state["current_index"] is not None - and (prev_state["current_index"] != new_state["current_index"] or end_of_queue_reached) - and (queue_item := self.get_item(queue_id, prev_state["current_index"])) - and (stream_details := queue_item.streamdetails) - ): - seconds_streamed = prev_state["elapsed_time"] - if music_prov := self.mass.get_provider(stream_details.provider): - if seconds_streamed > 10: - self.mass.create_task(music_prov.on_streamed(stream_details, seconds_streamed)) - if queue_item.media_item and seconds_streamed > 10: - # signal 'media item played' event, - # which is useful for plugins that want to do scrobbling - self.mass.signal_event( - EventType.MEDIA_ITEM_PLAYED, - object_id=queue_item.media_item.uri, - data=round(seconds_streamed, 2), - ) - - if end_of_queue_reached: - # end of queue reached, clear items - self.mass.call_later( - 5, self._check_clear_queue, queue, task_id=f"clear_queue_{queue_id}" - ) - - # clear 'next track enqueued' flag if new track is loaded - if prev_state["current_index"] != new_state["current_index"]: - queue.next_track_enqueued = None - - # watch dynamic radio items refill if needed - if "current_index" in changed_keys: - if ( - queue.dont_stop_the_music_enabled - and queue.enqueued_media_items - and queue.current_index is not None - and (queue.items - queue.current_index) <= 1 - ): - # We have received the last item in the queue and Don't stop the music is enabled - # set the played media item(s) as radio items (which will refill the queue) - # note that this will fail if there are no media items for which we have - # a dynamic radio source. - queue.radio_source = queue.enqueued_media_items - if ( - queue.radio_source - and queue.current_index is not None - and (queue.items - queue.current_index) < 5 - ): - task_id = f"fill_radio_tracks_{queue_id}" - self.mass.call_later(5, self._fill_radio_tracks, queue_id, task_id=task_id) - - def on_player_remove(self, player_id: str) -> None: - """Call when a player is removed from the registry.""" - self.mass.create_task(self.mass.cache.delete(f"queue.state.{player_id}")) - self.mass.create_task(self.mass.cache.delete(f"queue.items.{player_id}")) - self._queues.pop(player_id, None) - self._queue_items.pop(player_id, None) - - async def load_next_item( - self, - queue_id: str, - current_item_id_or_index: str | int | None = None, - ) -> QueueItem: - """Call when a player wants to (pre)load the next item into the buffer. - - Raises QueueEmpty if there are no more tracks left. - """ - queue = self.get(queue_id) - if not queue: - msg = f"PlayerQueue {queue_id} is not available" - raise PlayerUnavailableError(msg) - if current_item_id_or_index is None: - cur_index = queue.index_in_buffer or queue.current_index or 0 - elif isinstance(current_item_id_or_index, str): - cur_index = self.index_by_id(queue_id, current_item_id_or_index) - else: - cur_index = current_item_id_or_index - idx = 0 - while True: - next_item: QueueItem | None = None - next_index = self._get_next_index(queue_id, cur_index + idx) - if next_index is None: - raise QueueEmpty("No more tracks left in the queue.") - queue_item = self.get_item(queue_id, next_index) - if queue_item is None: - raise QueueEmpty("No more tracks left in the queue.") - - # work out if we are playing an album and if we should prefer album loudness - if ( - next_index is not None - and (next_item := self.get_item(queue_id, next_index)) - and ( - queue_item.media_item - and hasattr(queue_item.media_item, "album") - and queue_item.media_item.album - and next_item.media_item - and hasattr(next_item.media_item, "album") - and next_item.media_item.album - and queue_item.media_item.album.item_id == next_item.media_item.album.item_id - ) - ): - prefer_album_loudness = True - else: - prefer_album_loudness = False - - try: - # Check if the QueueItem is playable. For example, YT Music returns Radio Items - # that are not playable which will stop playback. - queue_item.streamdetails = await get_stream_details( - mass=self.mass, - queue_item=queue_item, - prefer_album_loudness=prefer_album_loudness, - ) - # Ensure we have at least an image for the queue item, - # so grab full item if needed. Note that for YTM this is always needed - # because it has poor thumbs by default (..sigh) - if queue_item.media_item and ( - not queue_item.media_item.image - or queue_item.media_item.provider.startswith("ytmusic") - ): - queue_item.media_item = await self.mass.music.get_item_by_uri(queue_item.uri) - # allow stripping silence from the begin/end of the track if crossfade is enabled - # this will allow for (much) smoother crossfades - if await self.mass.config.get_player_config_value(queue_id, CONF_CROSSFADE): - queue_item.streamdetails.strip_silence_end = True - queue_item.streamdetails.strip_silence_begin = True - # we're all set, this is our next item - next_item = queue_item - break - except MediaNotFoundError: - # No stream details found, skip this QueueItem - self.logger.debug("Skipping unplayable item: %s", next_item) - queue_item.streamdetails = StreamDetails( - provider=queue_item.media_item.provider if queue_item.media_item else "unknown", - item_id=queue_item.media_item.item_id if queue_item.media_item else "unknown", - audio_format=AudioFormat(), - media_type=queue_item.media_type, - seconds_streamed=0, - ) - idx += 1 - if next_item is None: - raise QueueEmpty("No more (playable) tracks left in the queue.") - return next_item - - def track_loaded_in_buffer(self, queue_id: str, item_id: str) -> None: - """Call when a player has (started) loading a track in the buffer.""" - queue = self.get(queue_id) - if not queue: - msg = f"PlayerQueue {queue_id} is not available" - raise PlayerUnavailableError(msg) - # store the index of the item that is currently (being) loaded in the buffer - # which helps us a bit to determine how far the player has buffered ahead - queue.index_in_buffer = self.index_by_id(queue_id, item_id) - if queue.flow_mode: - return # nothing to do when flow mode is active - self.signal_update(queue_id) - - # Main queue manipulation methods - - def load( - self, - queue_id: str, - queue_items: list[QueueItem], - insert_at_index: int = 0, - keep_remaining: bool = True, - keep_played: bool = True, - shuffle: bool = False, - ) -> None: - """Load new items at index. - - - queue_id: id of the queue to process this request. - - queue_items: a list of QueueItems - - insert_at_index: insert the item(s) at this index - - keep_remaining: keep the remaining items after the insert - - shuffle: (re)shuffle the items after insert index - """ - prev_items = self._queue_items[queue_id][:insert_at_index] if keep_played else [] - next_items = queue_items - - # if keep_remaining, append the old 'next' items - if keep_remaining: - next_items += self._queue_items[queue_id][insert_at_index:] - - # we set the original insert order as attribute so we can un-shuffle - for index, item in enumerate(next_items): - item.sort_index += insert_at_index + index - # (re)shuffle the final batch if needed - if shuffle: - next_items = random.sample(next_items, len(next_items)) - self.update_items(queue_id, prev_items + next_items) - - def update_items(self, queue_id: str, queue_items: list[QueueItem]) -> None: - """Update the existing queue items, mostly caused by reordering.""" - self._queue_items[queue_id] = queue_items - self._queues[queue_id].items = len(self._queue_items[queue_id]) - self.signal_update(queue_id, True) - self._queues[queue_id].next_track_enqueued = None - - # Helper methods - - def get_item(self, queue_id: str, item_id_or_index: int | str | None) -> QueueItem | None: - """Get queue item by index or item_id.""" - if item_id_or_index is None: - return None - queue_items = self._queue_items[queue_id] - if isinstance(item_id_or_index, int) and len(queue_items) > item_id_or_index: - return queue_items[item_id_or_index] - if isinstance(item_id_or_index, str): - return next((x for x in queue_items if x.queue_item_id == item_id_or_index), None) - return None - - def signal_update(self, queue_id: str, items_changed: bool = False) -> None: - """Signal state changed of given queue.""" - queue = self._queues[queue_id] - if items_changed: - self.mass.signal_event(EventType.QUEUE_ITEMS_UPDATED, object_id=queue_id, data=queue) - # save items in cache - self.mass.create_task( - self.mass.cache.set( - "items", - [x.to_cache() for x in self._queue_items[queue_id]], - category=CacheCategory.PLAYER_QUEUE_STATE, - base_key=queue_id, - ) - ) - # always send the base event - self.mass.signal_event(EventType.QUEUE_UPDATED, object_id=queue_id, data=queue) - # save state - self.mass.create_task( - self.mass.cache.set( - "state", - queue.to_cache(), - category=CacheCategory.PLAYER_QUEUE_STATE, - base_key=queue_id, - ) - ) - - def index_by_id(self, queue_id: str, queue_item_id: str) -> int | None: - """Get index by queue_item_id.""" - queue_items = self._queue_items[queue_id] - for index, item in enumerate(queue_items): - if item.queue_item_id == queue_item_id: - return index - return None - - def player_media_from_queue_item(self, queue_item: QueueItem, flow_mode: bool) -> PlayerMedia: - """Parse PlayerMedia from QueueItem.""" - media = PlayerMedia( - uri=self.mass.streams.resolve_stream_url(queue_item, flow_mode=flow_mode), - media_type=MediaType.FLOW_STREAM if flow_mode else queue_item.media_type, - title="Music Assistant" if flow_mode else queue_item.name, - image_url=MASS_LOGO_ONLINE, - duration=queue_item.duration, - queue_id=queue_item.queue_id, - queue_item_id=queue_item.queue_item_id, - ) - if not flow_mode and queue_item.media_item: - media.title = queue_item.media_item.name - media.artist = getattr(queue_item.media_item, "artist_str", "") - media.album = ( - album.name if (album := getattr(queue_item.media_item, "album", None)) else "" - ) - if queue_item.image: - media.image_url = self.mass.metadata.get_image_url(queue_item.image) - return media - - async def get_artist_tracks(self, artist: Artist) -> list[Track]: - """Return tracks for given artist, based on user preference.""" - artist_items_conf = self.mass.config.get_raw_core_config_value( - self.domain, - CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, - ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, - ) - self.logger.debug( - "Fetching tracks to play for artist %s", - artist.name, - ) - if artist_items_conf in ("library_tracks", "all_tracks"): - all_items = await self.mass.music.artists.tracks( - artist.item_id, - artist.provider, - in_library_only=artist_items_conf == "library_tracks", - ) - random.shuffle(all_items) - return all_items - - if artist_items_conf in ("library_album_tracks", "all_album_tracks"): - all_items: list[Track] = [] - for library_album in await self.mass.music.artists.albums( - artist.item_id, - artist.provider, - in_library_only=artist_items_conf == "library_album_tracks", - ): - for album_track in await self.mass.music.albums.tracks( - library_album.item_id, library_album.provider - ): - if album_track not in all_items: - all_items.append(album_track) - random.shuffle(all_items) - return all_items - - return [] - - async def get_album_tracks(self, album: Album, start_item: str | None) -> list[Track]: - """Return tracks for given album, based on user preference.""" - album_items_conf = self.mass.config.get_raw_core_config_value( - self.domain, - CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, - ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, - ) - result: list[Track] = [] - start_item_found = False - self.logger.debug( - "Fetching tracks to play for album %s", - album.name, - ) - for album_track in await self.mass.music.albums.tracks( - item_id=album.item_id, - provider_instance_id_or_domain=album.provider, - in_library_only=album_items_conf == "library_tracks", - ): - if not album_track.available: - continue - if start_item in (album_track.item_id, album_track.uri): - start_item_found = True - if start_item is not None and not start_item_found: - continue - result.append(album_track) - return result - - async def get_playlist_tracks(self, playlist: Playlist, start_item: str | None) -> list[Track]: - """Return tracks for given playlist, based on user preference.""" - result: list[Track] = [] - start_item_found = False - self.logger.debug( - "Fetching tracks to play for playlist %s", - playlist.name, - ) - # TODO: Handle other sort options etc. - async for playlist_track in self.mass.music.playlists.tracks( - playlist.item_id, playlist.provider - ): - if not playlist_track.available: - continue - if start_item in (playlist_track.item_id, playlist_track.uri): - start_item_found = True - if start_item is not None and not start_item_found: - continue - result.append(playlist_track) - return result - - def _get_next_index( - self, queue_id: str, cur_index: int | None, is_skip: bool = False, allow_repeat: bool = True - ) -> int | None: - """ - Return the next index for the queue, accounting for repeat settings. - - Will return None if there are no (more) items in the queue. - """ - queue = self._queues[queue_id] - queue_items = self._queue_items[queue_id] - if not queue_items or cur_index is None: - # queue is empty - return None - # handle repeat single track - if queue.repeat_mode == RepeatMode.ONE and not is_skip: - return cur_index if allow_repeat else None - # handle cur_index is last index of the queue - if cur_index >= (len(queue_items) - 1): - if allow_repeat and queue.repeat_mode == RepeatMode.ALL: - # if repeat all is enabled, we simply start again from the beginning - return 0 - return None - # all other: just the next index - return cur_index + 1 - - def _get_next_item(self, queue_id: str, cur_index: int | None = None) -> QueueItem | None: - """Return next QueueItem for given queue.""" - if (next_index := self._get_next_index(queue_id, cur_index)) is not None: - return self.get_item(queue_id, next_index) - return None - - async def _fill_radio_tracks(self, queue_id: str) -> None: - """Fill a Queue with (additional) Radio tracks.""" - tracks = await self._get_radio_tracks(queue_id=queue_id, is_initial_radio_mode=False) - # fill queue - filter out unavailable items - queue_items = [QueueItem.from_media_item(queue_id, x) for x in tracks if x.available] - self.load( - queue_id, - queue_items, - insert_at_index=len(self._queue_items[queue_id]) + 1, - ) - - def _check_enqueue_next(self, queue: PlayerQueue) -> None: - """Enqueue the next item in the queue (if needed).""" - if queue.flow_mode: - return - if queue.next_item is None: - return - if queue.next_track_enqueued == queue.next_item.queue_item_id: - return - - async def _enqueue_next(): - next_item = await self.load_next_item(queue.queue_id, queue.current_index) - queue.next_track_enqueued = next_item.queue_item_id - await self.mass.players.enqueue_next_media( - player_id=queue.queue_id, - media=self.player_media_from_queue_item(next_item, False), - ) - - self.mass.create_task(_enqueue_next()) - - async def _get_radio_tracks( - self, queue_id: str, is_initial_radio_mode: bool = False - ) -> list[Track]: - """Call the registered music providers for dynamic tracks.""" - queue = self._queues[queue_id] - if not queue.radio_source: - # this may happen during race conditions as this method is called delayed - return None - available_base_tracks: list[Track] = [] - base_track_sample_size = 5 - # Grab all the available base tracks based on the selected source items. - # shuffle the source items, just in case - for radio_item in random.sample(queue.radio_source, len(queue.radio_source)): - ctrl = self.mass.music.get_controller(radio_item.media_type) - try: - available_base_tracks += [ - track - for track in await ctrl.dynamic_base_tracks( - radio_item.item_id, radio_item.provider - ) - # Avoid duplicate base tracks - if track not in available_base_tracks - ] - except UnsupportedFeaturedException: - self.logger.debug( - "Skip loading radio items for %s: - " - "Provider %s does not support dynamic (base) tracks", - radio_item.uri, - radio_item.provider, - ) - # Sample tracks from the base tracks, which will be used to calculate the dynamic ones - base_tracks = random.sample( - available_base_tracks, min(base_track_sample_size, len(available_base_tracks)) - ) - # Use a set to avoid duplicate dynamic tracks - dynamic_tracks: set[Track] = set() - track_ctrl = self.mass.music.get_controller(MediaType.TRACK) - # Use base tracks + Trackcontroller to obtain similar tracks for every base Track - for base_track in base_tracks: - [ - dynamic_tracks.add(track) - for track in await track_ctrl.get_provider_similar_tracks( - base_track.item_id, base_track.provider - ) - if track not in base_tracks - # Ignore tracks that are too long for radio mode, e.g. mixes - and track.duration <= RADIO_TRACK_MAX_DURATION_SECS - ] - if len(dynamic_tracks) >= 50: - break - queue_tracks: list[Track] = [] - dynamic_tracks = list(dynamic_tracks) - # Only include the sampled base tracks when the radio mode is first initialized - if is_initial_radio_mode: - queue_tracks += [base_tracks[0]] - # Exhaust base tracks with the pattern of BDDBDDBDD (1 base track + 2 dynamic tracks) - if len(base_tracks) > 1: - for base_track in base_tracks[1:]: - queue_tracks += [base_track] - queue_tracks += random.sample(dynamic_tracks, 2) - # Add dynamic tracks to the queue, make sure to exclude already picked tracks - remaining_dynamic_tracks = [t for t in dynamic_tracks if t not in queue_tracks] - queue_tracks += random.sample( - remaining_dynamic_tracks, min(len(remaining_dynamic_tracks), 25) - ) - return queue_tracks - - async def _check_clear_queue(self, queue: PlayerQueue) -> None: - """Check if the queue should be cleared after the current item.""" - for _ in range(5): - await asyncio.sleep(1) - if queue.state != PlayerState.IDLE: - return - if queue.next_item is not None: - return - if not (queue.current_index >= len(self._queue_items[queue.queue_id]) - 1): - return - self.logger.info("End of queue reached, clearing items") - self.clear(queue.queue_id) - - def _get_flow_queue_stream_index( - self, queue: PlayerQueue, player: Player - ) -> tuple[int | None, int]: - """Calculate current queue index and current track elapsed time when flow mode is active.""" - elapsed_time_queue_total = player.corrected_elapsed_time or 0 - if queue.current_index is None: - return None, elapsed_time_queue_total - - # For each track that has been streamed/buffered to the player, - # a playlog entry will be created with the queue item id - # and the amount of seconds streamed. We traverse the playlog to figure - # out where we are in the queue, accounting for actual streamed - # seconds (and not duration) and skipped seconds. If a track has been repeated, - # it will simply be in the playlog multiple times. - played_time = 0 - queue_index = queue.current_index or 0 - track_time = 0 - for play_log_entry in queue.flow_mode_stream_log: - queue_item_duration = ( - # NOTE: 'seconds_streamed' can actually be 0 if there was a stream error! - play_log_entry.seconds_streamed - if play_log_entry.seconds_streamed is not None - else play_log_entry.duration - ) - if elapsed_time_queue_total > (queue_item_duration + played_time): - # total elapsed time is more than (streamed) track duration - # this track has been fully played, move in. - played_time += queue_item_duration - else: - # no more seconds left to divide, this is our track - # account for any seeking by adding the skipped/seeked seconds - queue_index = self.index_by_id(queue.queue_id, play_log_entry.queue_item_id) - queue_item = self.get_item(queue.queue_id, queue_index) - if queue_item and queue_item.streamdetails: - track_sec_skipped = queue_item.streamdetails.seek_position - else: - track_sec_skipped = 0 - track_time = elapsed_time_queue_total + track_sec_skipped - played_time - break - - return queue_index, track_time - - def _parse_player_current_item_id(self, queue_id: str, player: Player) -> str | None: - """Parse QueueItem ID from Player's current url.""" - if not player.current_media: - return None - if player.current_media.queue_id and player.current_media.queue_id != queue_id: - return None - if player.current_media.queue_item_id: - return player.current_media.queue_item_id - if not player.current_media.uri: - return None - if queue_id in player.current_media.uri: - # try to extract the item id from either a url or queue_id/item_id combi - current_item_id = player.current_media.uri.rsplit("/")[-1].split(".")[0] - if self.get_item(queue_id, current_item_id): - return current_item_id - return None diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py deleted file mode 100644 index 8bf6e3ce..00000000 --- a/music_assistant/server/controllers/players.py +++ /dev/null @@ -1,1314 +0,0 @@ -""" -MusicAssistant Players Controller. - -Handles all logic to control supported players, -which are provided by Player Providers. - -""" - -from __future__ import annotations - -import asyncio -import functools -import time -from contextlib import suppress -from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast - -from music_assistant.common.helpers.util import get_changed_values -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_ANNOUNCE_VOLUME, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, - CONF_ENTRY_PLAYER_ICON, - CONF_ENTRY_PLAYER_ICON_GROUP, - PlayerConfig, -) -from music_assistant.common.models.enums import ( - EventType, - MediaType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderType, -) -from music_assistant.common.models.errors import ( - AlreadyRegisteredError, - PlayerCommandFailed, - PlayerUnavailableError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.media_items import UniqueList -from music_assistant.common.models.player import Player, PlayerMedia -from music_assistant.constants import ( - CONF_AUTO_PLAY, - CONF_HIDE_PLAYER, - CONF_PLAYERS, - CONF_TTS_PRE_ANNOUNCE, -) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.core_controller import CoreController -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.player_group import PlayerGroupProvider - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Coroutine, Iterator - - from music_assistant.common.models.config_entries import CoreConfig - - -_PlayerControllerT = TypeVar("_PlayerControllerT", bound="PlayerController") -_R = TypeVar("_R") -_P = ParamSpec("_P") - - -def handle_player_command( - func: Callable[Concatenate[_PlayerControllerT, _P], Awaitable[_R]], -) -> Callable[Concatenate[_PlayerControllerT, _P], Coroutine[Any, Any, _R | None]]: - """Check and log commands to players.""" - - @functools.wraps(func) - async def wrapper(self: _PlayerControllerT, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: - """Log and handle_player_command commands to players.""" - player_id = kwargs["player_id"] if "player_id" in kwargs else args[0] - if (player := self._players.get(player_id)) is None or not player.available: - # player not existent - self.logger.warning( - "Ignoring command %s for unavailable player %s", - func.__name__, - player_id, - ) - return - - self.logger.debug( - "Handling command %s for player %s", - func.__name__, - player.display_name, - ) - try: - await func(self, *args, **kwargs) - except Exception as err: - raise PlayerCommandFailed(str(err)) from err - - return wrapper - - -class PlayerController(CoreController): - """Controller holding all logic to control registered players.""" - - domain: str = "players" - - def __init__(self, *args, **kwargs) -> None: - """Initialize core controller.""" - super().__init__(*args, **kwargs) - self._players: dict[str, Player] = {} - self._prev_states: dict[str, dict] = {} - self.manifest.name = "Players controller" - self.manifest.description = ( - "Music Assistant's core controller which manages all players from all providers." - ) - self.manifest.icon = "speaker-multiple" - self._poll_task: asyncio.Task | None = None - self._player_throttlers: dict[str, Throttler] = {} - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - self._poll_task = self.mass.create_task(self._poll_players()) - - async def close(self) -> None: - """Cleanup on exit.""" - if self._poll_task and not self._poll_task.done(): - self._poll_task.cancel() - - @property - def providers(self) -> list[PlayerProvider]: - """Return all loaded/running MusicProviders.""" - return self.mass.get_providers(ProviderType.MUSIC) # type: ignore=return-value - - def __iter__(self) -> Iterator[Player]: - """Iterate over (available) players.""" - return iter(self._players.values()) - - @api_command("players/all") - def all( - self, - return_unavailable: bool = True, - return_disabled: bool = False, - ) -> tuple[Player, ...]: - """Return all registered players.""" - return tuple( - player - for player in self._players.values() - if (player.available or return_unavailable) and (player.enabled or return_disabled) - ) - - @api_command("players/get") - def get( - self, - player_id: str, - raise_unavailable: bool = False, - ) -> Player | None: - """Return Player by player_id.""" - if player := self._players.get(player_id): - if (not player.available or not player.enabled) and raise_unavailable: - msg = f"Player {player_id} is not available" - raise PlayerUnavailableError(msg) - return player - if raise_unavailable: - msg = f"Player {player_id} is not available" - raise PlayerUnavailableError(msg) - return None - - @api_command("players/get_by_name") - def get_by_name(self, name: str) -> Player | None: - """Return Player by name or None if no match is found.""" - return next((x for x in self._players.values() if x.name == name), None) - - # Player commands - - @api_command("players/cmd/stop") - @handle_player_command - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player. - - - 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.stop(active_queue.queue_id) - return - # send to player provider - async with self._player_throttlers[player.player_id]: - if player_provider := self.get_player_provider(player.player_id): - await player_provider.cmd_stop(player.player_id) - - @api_command("players/cmd/play") - @handle_player_command - async def cmd_play(self, player_id: str) -> None: - """Send PLAY (unpause) command to given player. - - - 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 - active_source = player.active_source or player.player_id - if (active_queue := self.mass.player_queues.get(active_source)) and active_queue.items: - await self.mass.player_queues.play(active_queue.queue_id) - return - # send to player provider - player_provider = self.get_player_provider(player.player_id) - async with self._player_throttlers[player.player_id]: - await player_provider.cmd_play(player.player_id) - - @api_command("players/cmd/pause") - @handle_player_command - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player. - - - player_id: player_id of the player to handle the command. - """ - player = self._get_player_with_redirect(player_id) - if player.announcement_in_progress: - self.logger.warning("Ignore command: An announcement is in progress") - return - if PlayerFeature.PAUSE not in player.supported_features: - # if player does not support pause, we need to send stop - self.logger.info( - "Player %s does not support pause, using STOP instead", player.display_name - ) - await self.cmd_stop(player.player_id) - return - 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. - - - player_id: player_id of the player to handle the command. - """ - player = self._get_player_with_redirect(player_id) - if player.state == PlayerState.PLAYING: - await self.cmd_pause(player.player_id) - else: - await self.cmd_play(player.player_id) - - @api_command("players/cmd/seek") - async def cmd_seek(self, player_id: str, position: int) -> None: - """Handle SEEK command for given player. - - - player_id: player_id of the player to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - player = self._get_player_with_redirect(player_id) - # Redirect to queue controller if it is active - active_source = player.active_source or player.player_id - if active_queue := self.mass.player_queues.get(active_source): - await self.mass.player_queues.seek(active_queue.queue_id, position) - return - if PlayerFeature.SEEK not in player.supported_features: - msg = f"Player {player.display_name} does not support seeking" - raise UnsupportedFeaturedException(msg) - player_prov = self.get_player_provider(player.player_id) - await player_prov.cmd_seek(player.player_id, position) - - @api_command("players/cmd/next") - async def cmd_next_track(self, player_id: str) -> None: - """Handle NEXT TRACK command for given player.""" - player = self._get_player_with_redirect(player_id) - # Redirect to queue controller if it is active - active_source = player.active_source or player.player_id - if active_queue := self.mass.player_queues.get(active_source): - await self.mass.player_queues.next(active_queue.queue_id) - return - if PlayerFeature.NEXT_PREVIOUS not in player.supported_features: - msg = f"Player {player.display_name} does not support skipping to the next track." - raise UnsupportedFeaturedException(msg) - player_prov = self.get_player_provider(player.player_id) - await player_prov.cmd_next(player.player_id) - - @api_command("players/cmd/previous") - async def cmd_previous_track(self, player_id: str) -> None: - """Handle PREVIOUS TRACK command for given player.""" - player = self._get_player_with_redirect(player_id) - # Redirect to queue controller if it is active - active_source = player.active_source or player.player_id - if active_queue := self.mass.player_queues.get(active_source): - await self.mass.player_queues.previous(active_queue.queue_id) - return - if PlayerFeature.NEXT_PREVIOUS not in player.supported_features: - msg = f"Player {player.display_name} does not support skipping to the previous track." - raise UnsupportedFeaturedException(msg) - player_prov = self.get_player_provider(player.player_id) - await player_prov.cmd_previous(player.player_id) - - @api_command("players/cmd/power") - @handle_player_command - async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = False) -> None: - """Send POWER command to given player. - - - player_id: player_id of the player to handle the command. - - powered: bool if player should be powered on or off. - """ - player = self.get(player_id, True) - - if player.powered == powered: - return # nothing to do - - # unsync player at power off - player_was_synced = player.synced_to is not None - if not powered and (player.synced_to): - await self.cmd_unsync(player_id) - - # always stop player at power off - if ( - not powered - and not player_was_synced - and player.state in (PlayerState.PLAYING, PlayerState.PAUSED) - ): - await self.cmd_stop(player_id) - - # power off all synced childs when player is a sync leader - elif not powered and player.type == PlayerType.PLAYER and player.group_childs: - async with TaskManager(self.mass) as tg: - for member in self.iter_group_members(player, True): - tg.create_task(self.cmd_power(member.player_id, False)) - - # handle actual power command - if PlayerFeature.POWER in player.supported_features: - # player supports power command: forward to player provider - player_provider = self.get_player_provider(player_id) - async with self._player_throttlers[player_id]: - await player_provider.cmd_power(player_id, powered) - else: - # allow the stop command to process and prevent race conditions - await asyncio.sleep(0.2) - await self.mass.cache.set(player_id, powered, base_key="player_power") - - # always optimistically set the power state to update the UI - # as fast as possible and prevent race conditions - player.powered = powered - # reset active source on power off - if not powered: - player.active_source = None - - if not skip_update: - self.update(player_id) - - # handle 'auto play on power on' feature - if ( - not player.active_group - and powered - and self.mass.config.get_raw_player_config_value(player_id, CONF_AUTO_PLAY, False) - and player.active_source in (None, player_id) - ): - await self.mass.player_queues.resume(player_id) - - @api_command("players/cmd/volume_set") - @handle_player_command - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player. - - - player_id: player_id of the player to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - # TODO: Implement PlayerControl - player = self.get(player_id, True) - if player.type == PlayerType.GROUP: - # redirect to group volume control - await self.cmd_group_volume(player_id, volume_level) - return - if PlayerFeature.VOLUME_SET not in player.supported_features: - msg = f"Player {player.display_name} does not support volume_set" - raise UnsupportedFeaturedException(msg) - player_provider = self.get_player_provider(player_id) - async with self._player_throttlers[player_id]: - await player_provider.cmd_volume_set(player_id, volume_level) - - @api_command("players/cmd/volume_up") - @handle_player_command - async def cmd_volume_up(self, player_id: str) -> None: - """Send VOLUME_UP command to given player. - - - player_id: player_id of the player to handle the command. - """ - if not (player := self.get(player_id)): - return - if player.volume_level < 5 or player.volume_level > 95: - step_size = 1 - elif player.volume_level < 20 or player.volume_level > 80: - step_size = 2 - else: - step_size = 5 - new_volume = min(100, self._players[player_id].volume_level + step_size) - await self.cmd_volume_set(player_id, new_volume) - - @api_command("players/cmd/volume_down") - @handle_player_command - async def cmd_volume_down(self, player_id: str) -> None: - """Send VOLUME_DOWN command to given player. - - - player_id: player_id of the player to handle the command. - """ - if not (player := self.get(player_id)): - return - if player.volume_level < 5 or player.volume_level > 95: - step_size = 1 - elif player.volume_level < 20 or player.volume_level > 80: - step_size = 2 - else: - step_size = 5 - new_volume = max(0, self._players[player_id].volume_level - step_size) - await self.cmd_volume_set(player_id, new_volume) - - @api_command("players/cmd/group_volume") - @handle_player_command - async def cmd_group_volume(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given playergroup. - - Will send the new (average) volume level to group child's. - - player_id: player_id of the playergroup to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - group_player = self.get(player_id, True) - assert group_player - # handle group volume by only applying the volume to powered members - cur_volume = group_player.group_volume - new_volume = volume_level - volume_dif = new_volume - cur_volume - coros = [] - for child_player in self.iter_group_members( - group_player, only_powered=True, exclude_self=False - ): - if PlayerFeature.VOLUME_SET not in child_player.supported_features: - continue - cur_child_volume = child_player.volume_level - new_child_volume = int(cur_child_volume + volume_dif) - new_child_volume = max(0, new_child_volume) - new_child_volume = min(100, new_child_volume) - coros.append(self.cmd_volume_set(child_player.player_id, new_child_volume)) - await asyncio.gather(*coros) - - @api_command("players/cmd/group_volume_up") - @handle_player_command - async def cmd_group_volume_up(self, player_id: str) -> None: - """Send VOLUME_UP command to given playergroup. - - - player_id: player_id of the player to handle the command. - """ - group_player = self.get(player_id, True) - assert group_player - cur_volume = group_player.group_volume - if cur_volume < 5 or cur_volume > 95: - step_size = 1 - elif cur_volume < 20 or cur_volume > 80: - step_size = 2 - else: - step_size = 5 - new_volume = min(100, cur_volume + step_size) - await self.cmd_group_volume(player_id, new_volume) - - @api_command("players/cmd/group_volume_down") - @handle_player_command - async def cmd_group_volume_down(self, player_id: str) -> None: - """Send VOLUME_DOWN command to given playergroup. - - - player_id: player_id of the player to handle the command. - """ - group_player = self.get(player_id, True) - assert group_player - cur_volume = group_player.group_volume - if cur_volume < 5 or cur_volume > 95: - step_size = 1 - elif cur_volume < 20 or cur_volume > 80: - step_size = 2 - else: - step_size = 5 - new_volume = max(0, cur_volume - step_size) - await self.cmd_group_volume(player_id, new_volume) - - @api_command("players/cmd/volume_mute") - @handle_player_command - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME_MUTE command to given player. - - - player_id: player_id of the player to handle the command. - - muted: bool if player should be muted. - """ - player = self.get(player_id, True) - assert player - if PlayerFeature.VOLUME_MUTE not in player.supported_features: - self.logger.info( - "Player %s does not support muting, using volume instead", player.display_name - ) - if muted: - player._prev_volume_level = player.volume_level - player.volume_muted = True - await self.cmd_volume_set(player_id, 0) - else: - player.volume_muted = False - await self.cmd_volume_set(player_id, player._prev_volume_level) - return - player_provider = self.get_player_provider(player_id) - async with self._player_throttlers[player_id]: - await player_provider.cmd_volume_mute(player_id, muted) - - @api_command("players/cmd/play_announcement") - async def play_announcement( - self, - player_id: str, - url: str, - use_pre_announce: bool | None = None, - volume_level: int | None = None, - ) -> None: - """Handle playback of an announcement (url) on given player.""" - player = self.get(player_id, True) - if not url.startswith("http"): - raise PlayerCommandFailed("Only URLs are supported for announcements") - if player.announcement_in_progress: - raise PlayerCommandFailed( - f"An announcement is already in progress to player {player.display_name}" - ) - try: - # mark announcement_in_progress on player - player.announcement_in_progress = True - # determine if the player has native announcements support - native_announce_support = PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features - # determine pre-announce from (group)player config - if use_pre_announce is None and "tts" in url: - use_pre_announce = await self.mass.config.get_player_config_value( - player_id, - CONF_TTS_PRE_ANNOUNCE, - ) - # if player type is group with all members supporting announcements, - # we forward the request to each individual player - if player.type == PlayerType.GROUP and ( - all( - PlayerFeature.PLAY_ANNOUNCEMENT in x.supported_features - for x in self.iter_group_members(player) - ) - ): - # forward the request to each individual player - async with TaskManager(self.mass) as tg: - for group_member in player.group_childs: - tg.create_task( - self.play_announcement( - group_member, - url=url, - use_pre_announce=use_pre_announce, - volume_level=volume_level, - ) - ) - return - self.logger.info( - "Playback announcement to player %s (with pre-announce: %s): %s", - player.display_name, - use_pre_announce, - url, - ) - # create a PlayerMedia object for the announcement so - # we can send a regular play-media call downstream - announcement = PlayerMedia( - uri=self.mass.streams.get_announcement_url(player_id, url, use_pre_announce), - media_type=MediaType.ANNOUNCEMENT, - title="Announcement", - custom_data={"url": url, "use_pre_announce": use_pre_announce}, - ) - # handle native announce support - if native_announce_support: - if prov := self.mass.get_provider(player.provider): - await prov.play_announcement(player_id, announcement, volume_level) - return - # use fallback/default implementation - await self._play_announcement(player, announcement, volume_level) - finally: - player.announcement_in_progress = False - - @handle_player_command - async def play_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle PLAY MEDIA on given player. - - - player_id: player_id of the player to handle the command. - - media: The Media that needs to be played on the player. - """ - player = self._get_player_with_redirect(player_id) - # power on the player if needed - if not player.powered: - await self.cmd_power(player.player_id, True) - player_prov = self.get_player_provider(player.player_id) - await player_prov.play_media( - player_id=player.player_id, - media=media, - ) - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of a next media item on the player.""" - player = self.get(player_id, raise_unavailable=True) - if PlayerFeature.ENQUEUE not in player.supported_features: - raise UnsupportedFeaturedException( - f"Player {player.display_name} does not support enqueueing" - ) - player_prov = self.mass.get_provider(player.provider) - async with self._player_throttlers[player_id]: - await player_prov.enqueue_next_media(player_id=player_id, media=media) - - @api_command("players/cmd/sync") - @handle_player_command - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (leader) player/sync group. - If the player is already synced to another player, it will be unsynced there first. - If the target player itself is already synced to another player, this may fail. - If the player can not be synced with the given target player, this may fail. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup leader or group player. - """ - await self.cmd_sync_many(target_player, [player_id]) - - @api_command("players/cmd/unsync") - @handle_player_command - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - If the player is not currently synced to any other player, - this will silently be ignored. - - - player_id: player_id of the player to handle the command. - """ - if not (player := self.get(player_id)): - self.logger.warning("Player %s is not available", player_id) - return - if PlayerFeature.SYNC not in player.supported_features: - self.logger.warning("Player %s does not support (un)sync commands", player.name) - return - if not (player.synced_to or player.group_childs): - return # nothing to do - - if player.active_group and ( - (group_provider := self.get_player_provider(player.active_group)) - and group_provider.domain == "player_group" - ): - # the player is part of a permanent (sync)group and the user tries to unsync - # redirect the command to the group provider - group_provider = cast(PlayerGroupProvider, group_provider) - await group_provider.cmd_unsync_member(player_id, player.active_group) - return - - # handle (edge)case where un unsync command is sent to a sync leader; - # we dissolve the entire syncgroup in this case. - # while maybe not strictly needed to do this for all player providers, - # we do this to keep the functionality consistent across all providers - if player.group_childs: - self.logger.warning( - "Detected unsync command to player %s which is a sync(group) leader, " - "all sync members will be unsynced!", - player.name, - ) - async with TaskManager(self.mass) as tg: - for group_child_id in player.group_childs: - if group_child_id == player_id: - continue - tg.create_task(self.cmd_unsync(group_child_id)) - return - - # (optimistically) reset active source player if it is unsynced - player.active_source = None - - # forward command to the player provider - if player_provider := self.get_player_provider(player_id): - await player_provider.cmd_unsync(player_id) - # if the command succeeded we optimistically reset the sync state - # this is to prevent race conditions and to update the UI as fast as possible - player.synced_to = None - - @api_command("players/cmd/sync_many") - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - parent_player: Player = self.get(target_player, True) - prev_group_childs = parent_player.group_childs.copy() - if PlayerFeature.SYNC not in parent_player.supported_features: - msg = f"Player {parent_player.name} does not support sync commands" - raise UnsupportedFeaturedException(msg) - - if parent_player.synced_to: - # guard edge case: player already synced to another player - raise PlayerCommandFailed( - f"Player {parent_player.name} is already synced to another player on its own, " - "you need to unsync it first before you can join other players to it.", - ) - - # filter all player ids on compatibility and availability - final_player_ids: UniqueList[str] = UniqueList() - for child_player_id in child_player_ids: - if child_player_id == target_player: - continue - if not (child_player := self.get(child_player_id)) or not child_player.available: - self.logger.warning("Player %s is not available", child_player_id) - continue - if PlayerFeature.SYNC not in child_player.supported_features: - # this should not happen, but just in case bad things happen, guard it - self.logger.warning("Player %s does not support sync commands", child_player.name) - continue - if child_player.synced_to and child_player.synced_to == target_player: - continue # already synced to this target - - if child_player.group_childs and child_player.state != PlayerState.IDLE: - # guard edge case: childplayer is already a sync leader on its own - raise PlayerCommandFailed( - f"Player {child_player.name} is already synced with other players, " - "you need to unsync it first before you can join it to another player.", - ) - if child_player.synced_to: - # player already synced to another player, unsync first - self.logger.warning( - "Player %s is already synced to another player, unsyncing first", - child_player.name, - ) - await self.cmd_unsync(child_player.player_id) - # power on the player if needed - if not child_player.powered: - await self.cmd_power(child_player.player_id, True, skip_update=True) - # if we reach here, all checks passed - final_player_ids.append(child_player_id) - # set active source if player is synced - child_player.active_source = parent_player.player_id - - # forward command to the player provider after all (base) sanity checks - player_provider = self.get_player_provider(target_player) - async with self._player_throttlers[target_player]: - try: - await player_provider.cmd_sync_many(target_player, final_player_ids) - except Exception: - # restore sync state if the command failed - parent_player.group_childs = prev_group_childs - raise - - @api_command("players/cmd/unsync_many") - async def cmd_unsync_many(self, player_ids: list[str]) -> None: - """Handle UNSYNC command for all the given players.""" - for player_id in list(player_ids): - await self.cmd_unsync(player_id) - - def set(self, player: Player) -> None: - """Set/Update player details on the controller.""" - if player.player_id not in self._players: - # new player - self.register(player) - return - self._players[player.player_id] = player - self.update(player.player_id) - - async def register(self, player: Player) -> None: - """Register a new player on the controller.""" - if self.mass.closing: - return - player_id = player.player_id - - if player_id in self._players: - msg = f"Player {player_id} is already registered" - raise AlreadyRegisteredError(msg) - - # make sure that the player's provider is set to the instance id - if prov := self.mass.get_provider(player.provider): - player.provider = prov.instance_id - else: - raise RuntimeError("Invalid provider ID given: %s", player.provider) - - # make sure a default config exists - self.mass.config.create_default_player_config( - player_id, player.provider, player.name, player.enabled_by_default - ) - - player.enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True) - - # register playerqueue for this player - self.mass.create_task(self.mass.player_queues.on_player_register(player)) - - # register throttler for this player - self._player_throttlers[player_id] = Throttler(1, 0.2) - - self._players[player_id] = player - - # ignore disabled players - if not player.enabled: - return - - # restore powered state from cache - if player.state == PlayerState.PLAYING: - player.powered = True - elif (cache := await self.mass.cache.get(player_id, base_key="player_power")) is not None: - player.powered = cache - - self.logger.info( - "Player registered: %s/%s", - player_id, - player.name, - ) - self.mass.signal_event(EventType.PLAYER_ADDED, object_id=player.player_id, data=player) - # always call update to fix special attributes like display name, group volume etc. - self.update(player.player_id) - - async def register_or_update(self, player: Player) -> None: - """Register a new player on the controller or update existing one.""" - if self.mass.closing: - return - - if player.player_id in self._players: - self._players[player.player_id] = player - self.update(player.player_id) - return - - await self.register(player) - - def remove(self, player_id: str, cleanup_config: bool = True) -> None: - """Remove a player from the player manager.""" - player = self._players.pop(player_id, None) - if player is None: - return - self.logger.info("Player removed: %s", player.name) - self.mass.player_queues.on_player_remove(player_id) - if cleanup_config: - self.mass.config.remove(f"players/{player_id}") - self._prev_states.pop(player_id, None) - self.mass.signal_event(EventType.PLAYER_REMOVED, player_id) - - def update( - self, player_id: str, skip_forward: bool = False, force_update: bool = False - ) -> None: - """Update player state.""" - if self.mass.closing: - return - if player_id not in self._players: - return - player = self._players[player_id] - prev_state = self._prev_states.get(player_id, {}) - player.active_source = self._get_active_source(player) - player.volume_level = player.volume_level or 0 # guard for None volume - # correct group_members if needed - if player.group_childs == {player.player_id}: - player.group_childs = set() - # Auto correct player state if player is synced (or group child) - # This is because some players/providers do not accurately update this info - # for the sync child's. - if player.synced_to and (sync_leader := self.get(player.synced_to)): - player.state = sync_leader.state - player.elapsed_time = sync_leader.elapsed_time - player.elapsed_time_last_updated = sync_leader.elapsed_time_last_updated - # calculate group volume - player.group_volume = self._get_group_volume_level(player) - if player.type == PlayerType.GROUP: - player.volume_level = player.group_volume - # prefer any overridden name from config - player.display_name = ( - self.mass.config.get_raw_player_config_value(player.player_id, "name") - or player.name - or player.player_id - ) - player.hidden = self.mass.config.get_raw_player_config_value( - player.player_id, CONF_HIDE_PLAYER, False - ) - player.icon = self.mass.config.get_raw_player_config_value( - player.player_id, - CONF_ENTRY_PLAYER_ICON.key, - CONF_ENTRY_PLAYER_ICON_GROUP.default_value - if player.type == PlayerType.GROUP - else CONF_ENTRY_PLAYER_ICON.default_value, - ) - - # correct available state if needed - if not player.enabled: - player.available = False - - # basic throttle: do not send state changed events if player did not actually change - new_state = self._players[player_id].to_dict() - changed_values = get_changed_values( - prev_state, - new_state, - ignore_keys=[ - "elapsed_time_last_updated", - "seq_no", - "last_poll", - ], - ) - self._prev_states[player_id] = new_state - - if not player.enabled and not force_update: - # ignore updates for disabled players - return - - # always signal update to the playerqueue (regardless of changes) - self.mass.player_queues.on_player_update(player, changed_values) - - if len(changed_values) == 0 and not force_update: - return - - if changed_values.keys() != {"elapsed_time"} or force_update: - # ignore elapsed_time only changes - self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player) - - if skip_forward and not force_update: - return - - # handle player becoming unavailable - if "available" in changed_values and not player.available: - self._handle_player_unavailable(player) - - # update/signal group player(s) child's when group updates - for child_player in self.iter_group_members(player, exclude_self=True): - self.update(child_player.player_id, skip_forward=True) - # update/signal group player(s) when child updates - for group_player in self._get_player_groups(player, powered_only=False): - if player_prov := self.mass.get_provider(group_player.provider): - self.mass.create_task(player_prov.poll_player(group_player.player_id)) - - def get_player_provider(self, player_id: str) -> PlayerProvider: - """Return PlayerProvider for given player.""" - player = self._players[player_id] - player_provider = self.mass.get_provider(player.provider) - return cast(PlayerProvider, player_provider) - - def get_announcement_volume(self, player_id: str, volume_override: int | None) -> int | None: - """Get the (player specific) volume for a announcement.""" - volume_strategy = self.mass.config.get_raw_player_config_value( - player_id, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value, - ) - volume_strategy_volume = self.mass.config.get_raw_player_config_value( - player_id, - CONF_ENTRY_ANNOUNCE_VOLUME.key, - CONF_ENTRY_ANNOUNCE_VOLUME.default_value, - ) - volume_level = volume_override - if volume_level is None and volume_strategy == "absolute": - volume_level = volume_strategy_volume - elif volume_level is None and volume_strategy == "relative": - player = self.get(player_id) - volume_level = player.volume_level + volume_strategy_volume - elif volume_level is None and volume_strategy == "percentual": - player = self.get(player_id) - percentual = (player.volume_level / 100) * volume_strategy_volume - volume_level = player.volume_level + percentual - if volume_level is not None: - announce_volume_min = self.mass.config.get_raw_player_config_value( - player_id, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value, - ) - volume_level = max(announce_volume_min, volume_level) - announce_volume_max = self.mass.config.get_raw_player_config_value( - player_id, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value, - ) - volume_level = min(announce_volume_max, volume_level) - # ensure the result is an integer - return None if volume_level is None else int(volume_level) - - def iter_group_members( - self, - group_player: Player, - only_powered: bool = False, - only_playing: bool = False, - active_only: bool = False, - exclude_self: bool = True, - ) -> Iterator[Player]: - """Get (child) players attached to a group player or syncgroup.""" - for child_id in list(group_player.group_childs): - if child_player := self.get(child_id, False): - if not child_player.available or not child_player.enabled: - continue - if not (not only_powered or child_player.powered): - continue - if not (not active_only or child_player.active_group == group_player.player_id): - continue - if exclude_self and child_player.player_id == group_player.player_id: - continue - if not ( - not only_playing - or child_player.state in (PlayerState.PLAYING, PlayerState.PAUSED) - ): - continue - yield child_player - - async def wait_for_state( - self, - player: Player, - wanted_state: PlayerState, - timeout: float = 60.0, - minimal_time: float = 0, - ) -> None: - """Wait for the given player to reach the given state.""" - start_timestamp = time.time() - self.logger.debug( - "Waiting for player %s to reach state %s", player.display_name, wanted_state - ) - try: - async with asyncio.timeout(timeout): - while player.state != wanted_state: - await asyncio.sleep(0.1) - - except TimeoutError: - self.logger.debug( - "Player %s did not reach state %s within the timeout of %s seconds", - player.display_name, - wanted_state, - timeout, - ) - elapsed_time = round(time.time() - start_timestamp, 2) - if elapsed_time < minimal_time: - self.logger.debug( - "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...", - player.display_name, - wanted_state, - elapsed_time, - minimal_time, - ) - await asyncio.sleep(minimal_time - elapsed_time) - else: - self.logger.debug( - "Player %s reached state %s within %s seconds", - player.display_name, - wanted_state, - elapsed_time, - ) - - async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: - """Call (by config manager) when the configuration of a player changes.""" - player_disabled = "enabled" in changed_keys and not config.enabled - # signal player provider that the config changed - if player_provider := self.mass.get_provider(config.provider): - with suppress(PlayerUnavailableError): - await player_provider.on_player_config_change(config, changed_keys) - if not (player := self.get(config.player_id)): - return - if player_disabled: - # edge case: ensure that the player is powered off if the player gets disabled - await self.cmd_power(config.player_id, False) - player.available = False - # if the player was playing, restart playback - elif not player_disabled and player.state == PlayerState.PLAYING: - self.mass.call_later(1, self.mass.player_queues.resume, player.active_source) - # 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 - group_player = self.get(player.active_group) - with suppress(UnsupportedFeaturedException, PlayerCommandFailed): - await player_provider.set_members( - player.active_group, - [x for x in group_player.group_childs if x != player.player_id], - ) - player.enabled = config.enabled - - def _get_player_with_redirect(self, player_id: str) -> Player: - """Get player with check if playback related command should be redirected.""" - player = self.get(player_id, True) - if player.synced_to and (sync_leader := self.get(player.synced_to)): - self.logger.info( - "Player %s is synced to %s and can not accept " - "playback related commands itself, " - "redirected the command to the sync leader.", - player.name, - sync_leader.name, - ) - return sync_leader - if player.active_group and (active_group := self.get(player.active_group)): - self.logger.info( - "Player %s is part of a playergroup and can not accept " - "playback related commands itself, " - "redirected the command to the group leader.", - player.name, - ) - return active_group - return player - - def _get_player_groups( - self, player: Player, available_only: bool = True, powered_only: bool = False - ) -> Iterator[Player]: - """Return all groupplayers the given player belongs to.""" - for _player in self: - if _player.player_id == player.player_id: - continue - if _player.type != PlayerType.GROUP: - continue - if available_only and not _player.available: - continue - if powered_only and not _player.powered: - continue - if player.player_id in _player.group_childs: - yield _player - - def _get_active_source(self, player: Player) -> str: - """Return the active_source id for given player.""" - # if player is synced, return group leader's active source - if player.synced_to and (parent_player := self.get(player.synced_to)): - return parent_player.active_source - # if player has group active, return those details - if player.active_group and (group_player := self.get(player.active_group)): - return self._get_active_source(group_player) - # defaults to the player's own player id if no active source set - return player.active_source or player.player_id - - def _get_group_volume_level(self, player: Player) -> int: - """Calculate a group volume from the grouped members.""" - if len(player.group_childs) == 0: - # player is not a group or syncgroup - return player.volume_level - # calculate group volume from all (turned on) players - group_volume = 0 - active_players = 0 - for child_player in self.iter_group_members(player, only_powered=True, exclude_self=False): - if PlayerFeature.VOLUME_SET not in child_player.supported_features: - continue - group_volume += child_player.volume_level or 0 - active_players += 1 - if active_players: - group_volume = group_volume / active_players - return int(group_volume) - - def _handle_player_unavailable(self, player: Player) -> None: - """Handle a player becoming unavailable.""" - if player.synced_to: - self.mass.create_task(self.cmd_unsync(player.player_id)) - # also set this optimistically because the above command will most likely fail - player.synced_to = None - return - for group_child_id in player.group_childs: - if group_child_id == player.player_id: - continue - if child_player := self.get(group_child_id): - self.mass.create_task(self.cmd_power(group_child_id, False, True)) - # also set this optimistically because the above command will most likely fail - child_player.synced_to = None - player.group_childs = set() - if player.active_group and (group_player := self.get(player.active_group)): - # remove player from group if its part of a group - group_player = self.get(player.active_group) - if player.player_id in group_player.group_childs: - group_player.group_childs.remove(player.player_id) - - async def _play_announcement( - self, - player: Player, - announcement: PlayerMedia, - volume_level: int | None = None, - ) -> None: - """Handle (default/fallback) implementation of the play announcement feature. - - This default implementation will; - - stop playback of the current media (if needed) - - power on the player (if needed) - - raise the volume a bit - - play the announcement (from given url) - - wait for the player to finish playing - - restore the previous power and volume - - restore playback (if needed and if possible) - - This default implementation will only be used if the player - (provider) has no native support for the PLAY_ANNOUNCEMENT feature. - """ - prev_power = player.powered - prev_state = player.state - prev_synced_to = player.synced_to - queue = self.mass.player_queues.get(player.active_source) - prev_queue_active = queue and queue.active - prev_item_id = player.current_item_id - # unsync player if its currently synced - if prev_synced_to: - self.logger.debug( - "Announcement to player %s - unsyncing player...", - player.display_name, - ) - await self.cmd_unsync(player.player_id) - # stop player if its currently playing - elif prev_state in (PlayerState.PLAYING, PlayerState.PAUSED): - self.logger.debug( - "Announcement to player %s - stop existing content (%s)...", - player.display_name, - prev_item_id, - ) - await self.cmd_stop(player.player_id) - # wait for the player to stop - await self.wait_for_state(player, PlayerState.IDLE, 10, 0.4) - # adjust volume if needed - # in case of a (sync) group, we need to do this for all child players - prev_volumes: dict[str, int] = {} - async with TaskManager(self.mass) as tg: - for volume_player_id in player.group_childs or (player.player_id,): - if not (volume_player := self.get(volume_player_id)): - continue - # catch any players that have a different source active - if ( - volume_player.active_source - not in ( - player.active_source, - volume_player.player_id, - None, - ) - and volume_player.state == PlayerState.PLAYING - ): - self.logger.warning( - "Detected announcement to playergroup %s while group member %s is playing " - "other content, this may lead to unexpected behavior.", - player.display_name, - volume_player.display_name, - ) - tg.create_task(self.cmd_stop(volume_player.player_id)) - prev_volume = volume_player.volume_level - announcement_volume = self.get_announcement_volume(volume_player_id, volume_level) - temp_volume = announcement_volume or player.volume_level - if temp_volume != prev_volume: - prev_volumes[volume_player_id] = prev_volume - self.logger.debug( - "Announcement to player %s - setting temporary volume (%s)...", - volume_player.display_name, - announcement_volume, - ) - tg.create_task( - self.cmd_volume_set(volume_player.player_id, announcement_volume) - ) - # play the announcement - self.logger.debug( - "Announcement to player %s - playing the announcement on the player...", - player.display_name, - ) - await self.play_media(player_id=player.player_id, media=announcement) - # wait for the player(s) to play - await self.wait_for_state(player, PlayerState.PLAYING, 10, minimal_time=0.1) - # wait for the player to stop playing - if not announcement.duration: - media_info = await parse_tags(announcement.custom_data["url"]) - announcement.duration = media_info.duration or 60 - media_info.duration += 2 - await self.wait_for_state( - player, - PlayerState.IDLE, - max(announcement.duration * 2, 60), - announcement.duration + 2, - ) - self.logger.debug( - "Announcement to player %s - restore previous state...", player.display_name - ) - # restore volume - async with TaskManager(self.mass) as tg: - for volume_player_id, prev_volume in prev_volumes.items(): - tg.create_task(self.cmd_volume_set(volume_player_id, prev_volume)) - - await asyncio.sleep(0.2) - player.current_item_id = prev_item_id - # either power off the player or resume playing - if not prev_power: - await self.cmd_power(player.player_id, False) - return - elif prev_synced_to: - await self.cmd_sync(player.player_id, prev_synced_to) - elif prev_queue_active and prev_state == PlayerState.PLAYING: - await self.mass.player_queues.resume(queue.queue_id, True) - elif prev_state == PlayerState.PLAYING: - # player was playing something else - try to resume that here - self.logger.warning("Can not resume %s on %s", prev_item_id, player.display_name) - # TODO !! - - async def _poll_players(self) -> None: - """Background task that polls players for updates.""" - while True: - for player in list(self._players.values()): - player_id = player.player_id - # if the player is playing, update elapsed time every tick - # to ensure the queue has accurate details - player_playing = player.state == PlayerState.PLAYING - if player_playing: - self.mass.loop.call_soon(self.update, player_id) - # Poll player; - if not player.needs_poll: - continue - if (self.mass.loop.time() - player.last_poll) < player.poll_interval: - continue - player.last_poll = self.mass.loop.time() - if player_prov := self.get_player_provider(player_id): - try: - await player_prov.poll_player(player_id) - except PlayerUnavailableError: - player.available = False - player.state = PlayerState.IDLE - player.powered = False - except Exception as err: - self.logger.warning( - "Error while requesting latest state from player %s: %s", - player.display_name, - str(err), - exc_info=err if self.logger.isEnabledFor(10) else None, - ) - finally: - # always update player state - self.mass.loop.call_soon(self.update, player_id) - await asyncio.sleep(1) diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py deleted file mode 100644 index c13ab501..00000000 --- a/music_assistant/server/controllers/streams.py +++ /dev/null @@ -1,981 +0,0 @@ -""" -Controller to stream audio to players. - -The streams controller hosts a basic, unprotected HTTP-only webserver -purely to stream audio packets to players and some control endpoints such as -the upnp callbacks and json rpc api for slimproto clients. -""" - -from __future__ import annotations - -import os -import time -import urllib.parse -from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING - -from aiofiles.os import wrap -from aiohttp import web - -from music_assistant.common.helpers.util import get_ip, select_free_port, try_parse_bool -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_ENABLE_ICY_METADATA, - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - MediaType, - StreamType, - VolumeNormalizationMode, -) -from music_assistant.common.models.errors import QueueEmpty -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player_queue import PlayLogEntry -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import ( - ANNOUNCE_ALERT_FILE, - CONF_BIND_IP, - CONF_BIND_PORT, - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_HTTP_PROFILE, - CONF_OUTPUT_CHANNELS, - CONF_PUBLISH_IP, - CONF_SAMPLE_RATES, - CONF_VOLUME_NORMALIZATION, - CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO, - CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, - CONF_VOLUME_NORMALIZATION_RADIO, - CONF_VOLUME_NORMALIZATION_TRACKS, - MASS_LOGO_ONLINE, - SILENCE_FILE, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.helpers.audio import LOGGER as AUDIO_LOGGER -from music_assistant.server.helpers.audio import ( - check_audio_support, - crossfade_pcm_parts, - get_chunksize, - get_hls_substream, - get_icy_radio_stream, - get_media_stream, - get_player_filter_params, - get_silence, - get_stream_details, -) -from music_assistant.server.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER -from music_assistant.server.helpers.ffmpeg import get_ffmpeg_stream -from music_assistant.server.helpers.util import get_ips -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.core_controller import CoreController - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.common.models.player import Player - from music_assistant.common.models.player_queue import PlayerQueue - from music_assistant.common.models.queue_item import QueueItem - - -DEFAULT_STREAM_HEADERS = { - "Server": "Music Assistant", - "transferMode.dlna.org": "Streaming", - "contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501 - "Cache-Control": "no-cache", - "Pragma": "no-cache", -} -ICY_HEADERS = { - "icy-name": "Music Assistant", - "icy-description": "Music Assistant - Your personal music assistant", - "icy-version": "1", - "icy-logo": MASS_LOGO_ONLINE, -} -FLOW_DEFAULT_SAMPLE_RATE = 48000 -FLOW_DEFAULT_BIT_DEPTH = 24 - - -isfile = wrap(os.path.isfile) - - -def parse_pcm_info(content_type: str) -> tuple[int, int, int]: - """Parse PCM info from a codec/content_type string.""" - params = ( - dict(urllib.parse.parse_qsl(content_type.replace(";", "&"))) if ";" in content_type else {} - ) - sample_rate = int(params.get("rate", 44100)) - sample_size = int(params.get("bitrate", 16)) - channels = int(params.get("channels", 2)) - return (sample_rate, sample_size, channels) - - -class StreamsController(CoreController): - """Webserver Controller to stream audio to players.""" - - domain: str = "streams" - - def __init__(self, *args, **kwargs) -> None: - """Initialize instance.""" - super().__init__(*args, **kwargs) - self._server = Webserver(self.logger, enable_dynamic_routes=True) - self.register_dynamic_route = self._server.register_dynamic_route - self.unregister_dynamic_route = self._server.unregister_dynamic_route - self.manifest.name = "Streamserver" - self.manifest.description = ( - "Music Assistant's core controller that is responsible for " - "streaming audio to players on the local network." - ) - self.manifest.icon = "cast-audio" - self.announcements: dict[str, str] = {} - - @property - def base_url(self) -> str: - """Return the base_url for the streamserver.""" - return self._server.base_url - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - default_ip = await get_ip() - all_ips = await get_ips() - default_port = await select_free_port(8097, 9200) - return ( - ConfigEntry( - key=CONF_BIND_PORT, - type=ConfigEntryType.INTEGER, - default_value=default_port, - label="TCP Port", - description="The TCP port to run the server. " - "Make sure that this server can be reached " - "on the given IP and TCP port by players on the local network.", - ), - ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_RADIO, - type=ConfigEntryType.STRING, - default_value=VolumeNormalizationMode.FALLBACK_DYNAMIC, - label="Volume normalization method for radio streams", - options=( - ConfigValueOption(x.value.replace("_", " ").title(), x.value) - for x in VolumeNormalizationMode - ), - category="audio", - ), - ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_TRACKS, - type=ConfigEntryType.STRING, - default_value=VolumeNormalizationMode.FALLBACK_DYNAMIC, - label="Volume normalization method for tracks", - options=( - ConfigValueOption(x.value.replace("_", " ").title(), x.value) - for x in VolumeNormalizationMode - ), - category="audio", - ), - ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO, - type=ConfigEntryType.FLOAT, - range=(-20, 10), - default_value=-6, - label="Fixed/fallback gain adjustment for radio streams", - category="audio", - ), - ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, - type=ConfigEntryType.FLOAT, - range=(-20, 10), - default_value=-6, - label="Fixed/fallback gain adjustment for tracks", - category="audio", - ), - ConfigEntry( - key=CONF_PUBLISH_IP, - type=ConfigEntryType.STRING, - default_value=default_ip, - label="Published IP address", - description="This IP address is communicated to players where to find this server. " - "Override the default in advanced scenarios, such as multi NIC configurations. \n" - "Make sure that this server can be reached " - "on the given IP and TCP port by players on the local network. \n" - "This is an advanced setting that should normally " - "not be adjusted in regular setups.", - category="advanced", - ), - ConfigEntry( - key=CONF_BIND_IP, - type=ConfigEntryType.STRING, - default_value="0.0.0.0", - options=(ConfigValueOption(x, x) for x in {"0.0.0.0", *all_ips}), - label="Bind to IP/interface", - description="Start the stream server on this specific interface. \n" - "Use 0.0.0.0 to bind to all interfaces, which is the default. \n" - "This is an advanced setting that should normally " - "not be adjusted in regular setups.", - category="advanced", - ), - ) - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - ffmpeg_present, libsoxr_support, version = await check_audio_support() - major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) - if not ffmpeg_present: - self.logger.error("FFmpeg binary not found on your system, playback will NOT work!.") - elif major_version < 6: - self.logger.error("FFMpeg version is too old, you may run into playback issues.") - elif not libsoxr_support: - self.logger.warning( - "FFmpeg version found without libsoxr support, " - "highest quality audio not available. " - ) - self.logger.info( - "Detected ffmpeg version %s %s", - version, - "with libsoxr support" if libsoxr_support else "", - ) - # copy log level to audio/ffmpeg loggers - AUDIO_LOGGER.setLevel(self.logger.level) - FFMPEG_LOGGER.setLevel(self.logger.level) - # start the webserver - self.publish_port = config.get_value(CONF_BIND_PORT) - self.publish_ip = config.get_value(CONF_PUBLISH_IP) - await self._server.setup( - bind_ip=config.get_value(CONF_BIND_IP), - bind_port=self.publish_port, - base_url=f"http://{self.publish_ip}:{self.publish_port}", - static_routes=[ - ( - "*", - "/flow/{queue_id}/{queue_item_id}.{fmt}", - self.serve_queue_flow_stream, - ), - ( - "*", - "/single/{queue_id}/{queue_item_id}.{fmt}", - self.serve_queue_item_stream, - ), - ( - "*", - "/command/{queue_id}/{command}.mp3", - self.serve_command_request, - ), - ( - "*", - "/announcement/{player_id}.{fmt}", - self.serve_announcement_stream, - ), - ], - ) - - async def close(self) -> None: - """Cleanup on exit.""" - await self._server.close() - - def resolve_stream_url( - self, - queue_item: QueueItem, - flow_mode: bool = False, - output_codec: ContentType = ContentType.FLAC, - ) -> str: - """Resolve the stream URL for the given QueueItem.""" - fmt = output_codec.value - # handle raw pcm without exact format specifiers - if output_codec.is_pcm() and ";" not in fmt: - fmt += f";codec=pcm;rate={44100};bitrate={16};channels={2}" - query_params = {} - base_path = "flow" if flow_mode else "single" - url = f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}" # noqa: E501 - # we add a timestamp as basic checksum - # most importantly this is to invalidate any caches - # but also to handle edge cases such as single track repeat - query_params["ts"] = str(int(time.time())) - url += "?" + urllib.parse.urlencode(query_params) - return url - - async def serve_queue_item_stream(self, request: web.Request) -> web.Response: - """Stream single queueitem audio to a player.""" - self._log_request(request) - queue_id = request.match_info["queue_id"] - queue = self.mass.player_queues.get(queue_id) - if not queue: - raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}") - queue_player = self.mass.players.get(queue_id) - queue_item_id = request.match_info["queue_item_id"] - queue_item = self.mass.player_queues.get_item(queue_id, queue_item_id) - if not queue_item: - raise web.HTTPNotFound(reason=f"Unknown Queue item: {queue_item_id}") - if not queue_item.streamdetails: - try: - queue_item.streamdetails = await get_stream_details( - mass=self.mass, queue_item=queue_item - ) - except Exception as e: - self.logger.error( - "Failed to get streamdetails for QueueItem %s: %s", queue_item_id, e - ) - raise web.HTTPNotFound(reason=f"No streamdetails for Queue item: {queue_item_id}") - # work out output format/details - output_format = await self._get_output_format( - output_format_str=request.match_info["fmt"], - player=queue_player, - default_sample_rate=queue_item.streamdetails.audio_format.sample_rate, - default_bit_depth=queue_item.streamdetails.audio_format.bit_depth, - ) - - # prepare request, add some DLNA/UPNP compatible headers - headers = { - **DEFAULT_STREAM_HEADERS, - "icy-name": queue_item.name, - } - resp = web.StreamResponse( - status=200, - reason="OK", - headers=headers, - ) - resp.content_type = f"audio/{output_format.output_format_str}" - http_profile: str = await self.mass.config.get_player_config_value( - queue_id, CONF_HTTP_PROFILE - ) - if http_profile == "forced_content_length" and queue_item.duration: - # guess content length based on duration - resp.content_length = get_chunksize(output_format, queue_item.duration) - elif http_profile == "chunked": - resp.enable_chunked_encoding() - - await resp.prepare(request) - - # return early if this is not a GET request - if request.method != "GET": - return resp - - # all checks passed, start streaming! - self.logger.debug( - "Start serving audio stream for QueueItem %s (%s) to %s", - queue_item.name, - queue_item.uri, - queue.display_name, - ) - self.mass.player_queues.track_loaded_in_buffer(queue_id, queue_item_id) - - # pick pcm format based on the streamdetails and player capabilities - if self.mass.config.get_raw_player_config_value(queue_id, CONF_VOLUME_NORMALIZATION, True): - # prefer f32 when volume normalization is enabled - bit_depth = 32 - floating_point = True - else: - bit_depth = queue_item.streamdetails.audio_format.bit_depth - floating_point = False - pcm_format = AudioFormat( - content_type=ContentType.from_bit_depth(bit_depth, floating_point), - sample_rate=queue_item.streamdetails.audio_format.sample_rate, - bit_depth=bit_depth, - channels=2, - ) - chunk_num = 0 - async for chunk in get_ffmpeg_stream( - audio_input=self.get_media_stream( - streamdetails=queue_item.streamdetails, - pcm_format=pcm_format, - ), - input_format=pcm_format, - output_format=output_format, - filter_params=get_player_filter_params(self.mass, queue_player.player_id), - # we don't allow the player to buffer too much ahead so we use readrate limiting - extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], - ): - try: - await resp.write(chunk) - chunk_num += 1 - except (BrokenPipeError, ConnectionResetError, ConnectionError): - break - if queue_item.streamdetails.stream_error: - self.logger.error( - "Error streaming QueueItem %s (%s) to %s", - queue_item.name, - queue_item.uri, - queue.display_name, - ) - return resp - - async def serve_queue_flow_stream(self, request: web.Request) -> web.Response: - """Stream Queue Flow audio to player.""" - self._log_request(request) - queue_id = request.match_info["queue_id"] - queue = self.mass.player_queues.get(queue_id) - if not queue: - raise web.HTTPNotFound(reason=f"Unknown Queue: {queue_id}") - if not (queue_player := self.mass.players.get(queue_id)): - raise web.HTTPNotFound(reason=f"Unknown Player: {queue_id}") - start_queue_item_id = request.match_info["queue_item_id"] - start_queue_item = self.mass.player_queues.get_item(queue_id, start_queue_item_id) - if not start_queue_item: - raise web.HTTPNotFound(reason=f"Unknown Queue item: {start_queue_item_id}") - - # select the highest possible PCM settings for this player - flow_pcm_format = await self._select_flow_format(queue_player) - - # work out output format/details - output_format = await self._get_output_format( - output_format_str=request.match_info["fmt"], - player=queue_player, - default_sample_rate=flow_pcm_format.sample_rate, - default_bit_depth=flow_pcm_format.bit_depth, - ) - # work out ICY metadata support - icy_preference = self.mass.config.get_raw_player_config_value( - queue_id, - CONF_ENTRY_ENABLE_ICY_METADATA.key, - CONF_ENTRY_ENABLE_ICY_METADATA.default_value, - ) - enable_icy = request.headers.get("Icy-MetaData", "") == "1" and icy_preference != "disabled" - icy_meta_interval = 256000 if icy_preference == "full" else 16384 - - # prepare request, add some DLNA/UPNP compatible headers - headers = { - **DEFAULT_STREAM_HEADERS, - **ICY_HEADERS, - "Accept-Ranges": "none", - "Content-Type": f"audio/{output_format.output_format_str}", - } - if enable_icy: - headers["icy-metaint"] = str(icy_meta_interval) - - resp = web.StreamResponse( - status=200, - reason="OK", - headers=headers, - ) - http_profile: str = await self.mass.config.get_player_config_value( - queue_id, CONF_HTTP_PROFILE - ) - if http_profile == "forced_content_length": - # just set an insane high content length to make sure the player keeps playing - resp.content_length = get_chunksize(output_format, 12 * 3600) - elif http_profile == "chunked": - resp.enable_chunked_encoding() - - await resp.prepare(request) - - # return early if this is not a GET request - if request.method != "GET": - return resp - - # all checks passed, start streaming! - self.logger.debug("Start serving Queue flow audio stream for %s", queue.display_name) - - async for chunk in get_ffmpeg_stream( - audio_input=self.get_flow_stream( - queue=queue, start_queue_item=start_queue_item, pcm_format=flow_pcm_format - ), - input_format=flow_pcm_format, - output_format=output_format, - filter_params=get_player_filter_params(self.mass, queue_player.player_id), - chunk_size=icy_meta_interval if enable_icy else None, - # we don't allow the player to buffer too much ahead so we use readrate limiting - extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], - ): - try: - await resp.write(chunk) - except (BrokenPipeError, ConnectionResetError, ConnectionError): - # race condition - break - - if not enable_icy: - continue - - # if icy metadata is enabled, send the icy metadata after the chunk - if ( - # use current item here and not buffered item, otherwise - # the icy metadata will be too much ahead - (current_item := queue.current_item) - and current_item.streamdetails - and current_item.streamdetails.stream_title - ): - title = current_item.streamdetails.stream_title - elif queue and current_item and current_item.name: - title = current_item.name - else: - title = "Music Assistant" - metadata = f"StreamTitle='{title}';".encode() - if icy_preference == "full" and current_item and current_item.image: - metadata += f"StreamURL='{current_item.image.path}'".encode() - while len(metadata) % 16 != 0: - metadata += b"\x00" - length = len(metadata) - length_b = chr(int(length / 16)).encode() - await resp.write(length_b + metadata) - - return resp - - async def serve_command_request(self, request: web.Request) -> web.Response: - """Handle special 'command' request for a player.""" - self._log_request(request) - queue_id = request.match_info["queue_id"] - command = request.match_info["command"] - if command == "next": - self.mass.create_task(self.mass.player_queues.next(queue_id)) - return web.FileResponse(SILENCE_FILE, headers={"icy-name": "Music Assistant"}) - - async def serve_announcement_stream(self, request: web.Request) -> web.Response: - """Stream announcement audio to a player.""" - self._log_request(request) - player_id = request.match_info["player_id"] - player = self.mass.player_queues.get(player_id) - if not player: - raise web.HTTPNotFound(reason=f"Unknown Player: {player_id}") - if player_id not in self.announcements: - raise web.HTTPNotFound(reason=f"No pending announcements for Player: {player_id}") - announcement_url = self.announcements[player_id] - use_pre_announce = try_parse_bool(request.query.get("pre_announce")) - - # work out output format/details - fmt = request.match_info.get("fmt", announcement_url.rsplit(".")[-1]) - audio_format = AudioFormat(content_type=ContentType.try_parse(fmt)) - - http_profile: str = await self.mass.config.get_player_config_value( - player_id, CONF_HTTP_PROFILE - ) - if http_profile == "forced_content_length": - # given the fact that an announcement is just a short audio clip, - # just send it over completely at once so we have a fixed content length - data = b"" - async for chunk in self.get_announcement_stream( - announcement_url=announcement_url, - output_format=audio_format, - use_pre_announce=use_pre_announce, - ): - data += chunk - return web.Response( - body=data, - content_type=f"audio/{audio_format.output_format_str}", - headers=DEFAULT_STREAM_HEADERS, - ) - - resp = web.StreamResponse( - status=200, - reason="OK", - headers=DEFAULT_STREAM_HEADERS, - ) - resp.content_type = f"audio/{audio_format.output_format_str}" - if http_profile == "chunked": - resp.enable_chunked_encoding() - - await resp.prepare(request) - - # return early if this is not a GET request - if request.method != "GET": - return resp - - # all checks passed, start streaming! - self.logger.debug( - "Start serving audio stream for Announcement %s to %s", - announcement_url, - player.display_name, - ) - async for chunk in self.get_announcement_stream( - announcement_url=announcement_url, - output_format=audio_format, - use_pre_announce=use_pre_announce, - ): - try: - await resp.write(chunk) - except (BrokenPipeError, ConnectionResetError): - break - - self.logger.debug( - "Finished serving audio stream for Announcement %s to %s", - announcement_url, - player.display_name, - ) - - return resp - - def get_command_url(self, player_or_queue_id: str, command: str) -> str: - """Get the url for the special command stream.""" - return f"{self.base_url}/command/{player_or_queue_id}/{command}.mp3" - - def get_announcement_url( - self, - player_id: str, - announcement_url: str, - use_pre_announce: bool = False, - content_type: ContentType = ContentType.MP3, - ) -> str: - """Get the url for the special announcement stream.""" - self.announcements[player_id] = announcement_url - # use stream server to host announcement on local network - # this ensures playback on all players, including ones that do not - # like https hosts and it also offers the pre-announce 'bell' - return f"{self.base_url}/announcement/{player_id}.{content_type.value}?pre_announce={use_pre_announce}" # noqa: E501 - - async def get_flow_stream( - self, - queue: PlayerQueue, - start_queue_item: QueueItem, - pcm_format: AudioFormat, - ) -> AsyncGenerator[bytes, None]: - """Get a flow stream of all tracks in the queue as raw PCM audio.""" - # ruff: noqa: PLR0915 - assert pcm_format.content_type.is_pcm() - queue_track = None - last_fadeout_part = b"" - queue.flow_mode = True - use_crossfade = await self.mass.config.get_player_config_value( - queue.queue_id, CONF_CROSSFADE - ) - if not start_queue_item: - # this can happen in some (edge case) race conditions - return - if start_queue_item.media_type != MediaType.TRACK: - use_crossfade = False - pcm_sample_size = int( - pcm_format.sample_rate * (pcm_format.bit_depth / 8) * pcm_format.channels - ) - self.logger.info( - "Start Queue Flow stream for Queue %s - crossfade: %s", - queue.display_name, - use_crossfade, - ) - total_bytes_sent = 0 - - while True: - # get (next) queue item to stream - if queue_track is None: - queue_track = start_queue_item - else: - try: - queue_track = await self.mass.player_queues.load_next_item(queue.queue_id) - except QueueEmpty: - break - - if queue_track.streamdetails is None: - raise RuntimeError( - "No Streamdetails known for queue item %s", queue_track.queue_item_id - ) - - self.logger.debug( - "Start Streaming queue track: %s (%s) for queue %s", - queue_track.streamdetails.uri, - queue_track.name, - queue.display_name, - ) - self.mass.player_queues.track_loaded_in_buffer( - queue.queue_id, queue_track.queue_item_id - ) - # append to play log so the queue controller can work out which track is playing - play_log_entry = PlayLogEntry(queue_track.queue_item_id) - queue.flow_mode_stream_log.append(play_log_entry) - - # set some basic vars - pcm_sample_size = int(pcm_format.sample_rate * (pcm_format.bit_depth / 8) * 2) - crossfade_duration = self.mass.config.get_raw_player_config_value( - queue.queue_id, CONF_CROSSFADE_DURATION, 10 - ) - crossfade_size = int(pcm_sample_size * crossfade_duration) - bytes_written = 0 - buffer = b"" - # handle incoming audio chunks - async for chunk in self.get_media_stream( - queue_track.streamdetails, - pcm_format=pcm_format, - ): - # buffer size needs to be big enough to include the crossfade part - req_buffer_size = pcm_sample_size * 2 if not use_crossfade else crossfade_size - - # ALWAYS APPEND CHUNK TO BUFFER - buffer += chunk - del chunk - if len(buffer) < req_buffer_size: - # buffer is not full enough, move on - continue - - #### HANDLE CROSSFADE OF PREVIOUS TRACK AND NEW TRACK - if last_fadeout_part: - # perform crossfade - fadein_part = buffer[:crossfade_size] - remaining_bytes = buffer[crossfade_size:] - crossfade_part = await crossfade_pcm_parts( - fadein_part, - last_fadeout_part, - pcm_format=pcm_format, - ) - # send crossfade_part (as one big chunk) - bytes_written += len(crossfade_part) - yield crossfade_part - - # also write the leftover bytes from the crossfade action - if remaining_bytes: - yield remaining_bytes - bytes_written += len(remaining_bytes) - del remaining_bytes - # clear vars - last_fadeout_part = b"" - buffer = b"" - - #### OTHER: enough data in buffer, feed to output - while len(buffer) > req_buffer_size: - yield buffer[:pcm_sample_size] - bytes_written += pcm_sample_size - buffer = buffer[pcm_sample_size:] - - #### HANDLE END OF TRACK - if last_fadeout_part: - # edge case: we did not get enough data to make the crossfade - yield last_fadeout_part - bytes_written += len(last_fadeout_part) - last_fadeout_part = b"" - if use_crossfade: - # if crossfade is enabled, save fadeout part to pickup for next track - last_fadeout_part = buffer[-crossfade_size:] - remaining_bytes = buffer[:-crossfade_size] - if remaining_bytes: - yield remaining_bytes - bytes_written += len(remaining_bytes) - del remaining_bytes - elif buffer: - # no crossfade enabled, just yield the buffer last part - bytes_written += len(buffer) - yield buffer - # make sure the buffer gets cleaned up - del buffer - - # update duration details based on the actual pcm data we sent - # this also accounts for crossfade and silence stripping - seconds_streamed = bytes_written / pcm_sample_size - queue_track.streamdetails.seconds_streamed = seconds_streamed - queue_track.streamdetails.duration = ( - queue_track.streamdetails.seek_position + seconds_streamed - ) - play_log_entry.seconds_streamed = seconds_streamed - play_log_entry.duration = queue_track.streamdetails.duration - total_bytes_sent += bytes_written - self.logger.debug( - "Finished Streaming queue track: %s (%s) on queue %s", - queue_track.streamdetails.uri, - queue_track.name, - queue.display_name, - ) - #### HANDLE END OF QUEUE FLOW STREAM - # end of queue flow: make sure we yield the last_fadeout_part - if last_fadeout_part: - yield last_fadeout_part - # correct seconds streamed/duration - last_part_seconds = len(last_fadeout_part) / pcm_sample_size - queue_track.streamdetails.seconds_streamed += last_part_seconds - queue_track.streamdetails.duration += last_part_seconds - del last_fadeout_part - total_bytes_sent += bytes_written - self.logger.info("Finished Queue Flow stream for Queue %s", queue.display_name) - - async def get_announcement_stream( - self, announcement_url: str, output_format: AudioFormat, use_pre_announce: bool = False - ) -> AsyncGenerator[bytes, None]: - """Get the special announcement stream.""" - # work out output format/details - fmt = announcement_url.rsplit(".")[-1] - audio_format = AudioFormat(content_type=ContentType.try_parse(fmt)) - extra_args = [] - filter_params = ["loudnorm=I=-10:LRA=11:TP=-2"] - if use_pre_announce: - extra_args += [ - "-i", - ANNOUNCE_ALERT_FILE, - "-filter_complex", - "[1:a][0:a]concat=n=2:v=0:a=1,loudnorm=I=-10:LRA=11:TP=-1.5", - ] - filter_params = [] - async for chunk in get_ffmpeg_stream( - audio_input=announcement_url, - input_format=audio_format, - output_format=output_format, - extra_args=extra_args, - filter_params=filter_params, - ): - yield chunk - - async def get_media_stream( - self, - streamdetails: StreamDetails, - pcm_format: AudioFormat, - ) -> AsyncGenerator[tuple[bool, bytes], None]: - """Get the audio stream for the given streamdetails as raw pcm chunks.""" - is_radio = streamdetails.media_type == MediaType.RADIO or not streamdetails.duration - if is_radio: - streamdetails.seek_position = 0 - # collect all arguments for ffmpeg - filter_params = [] - extra_input_args = [] - # handle volume normalization - enable_volume_normalization = ( - streamdetails.target_loudness is not None - and streamdetails.volume_normalization_mode != VolumeNormalizationMode.DISABLED - ) - dynamic_volume_normalization = ( - streamdetails.volume_normalization_mode == VolumeNormalizationMode.DYNAMIC - and enable_volume_normalization - ) - if dynamic_volume_normalization: - # volume normalization using loudnorm filter (in dynamic mode) - # which also collects the measurement on the fly during playback - # more info: https://k.ylo.ph/2016/04/04/loudnorm.html - filter_rule = f"loudnorm=I={streamdetails.target_loudness}:TP=-2.0:LRA=10.0:offset=0.0" - filter_rule += ":print_format=json" - filter_params.append(filter_rule) - elif ( - enable_volume_normalization - and streamdetails.volume_normalization_mode == VolumeNormalizationMode.FIXED_GAIN - ): - # apply used defined fixed volume/gain correction - gain_correct: float = await self.mass.config.get_core_config_value( - CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO - if streamdetails.media_type == MediaType.RADIO - else CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, - ) - gain_correct = round(gain_correct, 2) - filter_params.append(f"volume={gain_correct}dB") - elif enable_volume_normalization and streamdetails.loudness is not None: - # volume normalization with known loudness measurement - # apply volume/gain correction - gain_correct = streamdetails.target_loudness - streamdetails.loudness - gain_correct = round(gain_correct, 2) - filter_params.append(f"volume={gain_correct}dB") - - # work out audio source for these streamdetails - if streamdetails.stream_type == StreamType.CUSTOM: - audio_source = self.mass.get_provider(streamdetails.provider).get_audio_stream( - streamdetails, - seek_position=streamdetails.seek_position, - ) - elif streamdetails.stream_type == StreamType.ICY: - audio_source = get_icy_radio_stream(self.mass, streamdetails.path, streamdetails) - elif streamdetails.stream_type == StreamType.HLS: - substream = await get_hls_substream(self.mass, streamdetails.path) - audio_source = substream.path - if streamdetails.media_type == MediaType.RADIO: - # Especially the BBC streams struggle when they're played directly - # with ffmpeg, where they just stop after some minutes, - # so we tell ffmpeg to loop around in this case. - extra_input_args += ["-stream_loop", "-1", "-re"] - else: - audio_source = streamdetails.path - - # add support for decryption key provided in streamdetails - if streamdetails.decryption_key: - extra_input_args += ["-decryption_key", streamdetails.decryption_key] - - # handle seek support - if ( - streamdetails.seek_position - and streamdetails.media_type != MediaType.RADIO - and streamdetails.stream_type != StreamType.CUSTOM - ): - extra_input_args += ["-ss", str(int(streamdetails.seek_position))] - - if streamdetails.media_type == MediaType.RADIO: - # pad some silence before the radio stream starts to create some headroom - # for radio stations that do not provide any look ahead buffer - # without this, some radio streams jitter a lot, especially with dynamic normalization - pad_seconds = 5 if dynamic_volume_normalization else 2 - async for chunk in get_silence(pad_seconds, pcm_format): - yield chunk - - async for chunk in get_media_stream( - self.mass, - streamdetails=streamdetails, - pcm_format=pcm_format, - audio_source=audio_source, - filter_params=filter_params, - extra_input_args=extra_input_args, - ): - yield chunk - - def _log_request(self, request: web.Request) -> None: - """Log request.""" - if not self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - return - self.logger.log( - VERBOSE_LOG_LEVEL, - "Got %s request to %s from %s\nheaders: %s\n", - request.method, - request.path, - request.remote, - request.headers, - ) - - async def _get_output_format( - self, - output_format_str: str, - player: Player, - default_sample_rate: int, - default_bit_depth: int, - ) -> AudioFormat: - """Parse (player specific) output format details for given format string.""" - content_type: ContentType = ContentType.try_parse(output_format_str) - supported_rates_conf = await self.mass.config.get_player_config_value( - player.player_id, CONF_SAMPLE_RATES - ) - supported_sample_rates: tuple[int] = tuple(x[0] for x in supported_rates_conf) - supported_bit_depths: tuple[int] = tuple(x[1] for x in supported_rates_conf) - player_max_bit_depth = max(supported_bit_depths) - if content_type.is_pcm() or content_type == ContentType.WAV: - # parse pcm details from format string - output_sample_rate, output_bit_depth, output_channels = parse_pcm_info( - output_format_str - ) - if content_type == ContentType.PCM: - # resolve generic pcm type - content_type = ContentType.from_bit_depth(output_bit_depth) - else: - if default_sample_rate in supported_sample_rates: - output_sample_rate = default_sample_rate - else: - output_sample_rate = max(supported_sample_rates) - output_bit_depth = min(default_bit_depth, player_max_bit_depth) - output_channels_str = self.mass.config.get_raw_player_config_value( - player.player_id, CONF_OUTPUT_CHANNELS, "stereo" - ) - output_channels = 1 if output_channels_str != "stereo" else 2 - if not content_type.is_lossless(): - output_bit_depth = 16 - output_sample_rate = min(48000, output_sample_rate) - return AudioFormat( - content_type=content_type, - sample_rate=output_sample_rate, - bit_depth=output_bit_depth, - channels=output_channels, - output_format_str=output_format_str, - ) - - async def _select_flow_format( - self, - player: Player, - ) -> AudioFormat: - """Parse (player specific) flow stream PCM format.""" - supported_rates_conf = await self.mass.config.get_player_config_value( - player.player_id, CONF_SAMPLE_RATES - ) - supported_sample_rates: tuple[int] = tuple(x[0] for x in supported_rates_conf) - supported_bit_depths: tuple[int] = tuple(x[1] for x in supported_rates_conf) - player_max_bit_depth = max(supported_bit_depths) - for sample_rate in (192000, 96000, 48000, 44100): - if sample_rate in supported_sample_rates: - output_sample_rate = sample_rate - break - if self.mass.config.get_raw_player_config_value( - player.player_id, CONF_VOLUME_NORMALIZATION, True - ): - # prefer f32 when volume normalization is enabled - output_bit_depth = 32 - floating_point = True - else: - output_bit_depth = min(24, player_max_bit_depth) - floating_point = False - return AudioFormat( - content_type=ContentType.from_bit_depth(output_bit_depth, floating_point), - sample_rate=output_sample_rate, - bit_depth=output_bit_depth, - channels=2, - ) diff --git a/music_assistant/server/controllers/webserver.py b/music_assistant/server/controllers/webserver.py deleted file mode 100644 index f117f779..00000000 --- a/music_assistant/server/controllers/webserver.py +++ /dev/null @@ -1,409 +0,0 @@ -""" -Controller that manages the builtin webserver that hosts the api and frontend. - -Unlike the streamserver (which is as simple and unprotected as possible), -this webserver allows for more fine grained configuration to better secure it. -""" - -from __future__ import annotations - -import asyncio -import logging -import os -import urllib.parse -from concurrent import futures -from contextlib import suppress -from functools import partial -from typing import TYPE_CHECKING, Any, Final - -from aiohttp import WSMsgType, web -from music_assistant_frontend import where as locate_frontend - -from music_assistant.common.helpers.util import get_ip -from music_assistant.common.models.api import ( - CommandMessage, - ErrorResultMessage, - MessageType, - SuccessResultMessage, -) -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueOption -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import InvalidCommand -from music_assistant.constants import CONF_BIND_IP, CONF_BIND_PORT, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.api import APICommandHandler, parse_arguments -from music_assistant.server.helpers.audio import get_preview_stream -from music_assistant.server.helpers.util import get_ips -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.core_controller import CoreController - -if TYPE_CHECKING: - from collections.abc import Awaitable - - from music_assistant.common.models.config_entries import ConfigValueType, CoreConfig - from music_assistant.common.models.event import MassEvent - -DEFAULT_SERVER_PORT = 8095 -CONF_BASE_URL = "base_url" -CONF_EXPOSE_SERVER = "expose_server" -MAX_PENDING_MSG = 512 -CANCELLATION_ERRORS: Final = (asyncio.CancelledError, futures.CancelledError) - - -class WebserverController(CoreController): - """Core Controller that manages the builtin webserver that hosts the api and frontend.""" - - domain: str = "webserver" - - def __init__(self, *args, **kwargs) -> None: - """Initialize instance.""" - super().__init__(*args, **kwargs) - self._server = Webserver(self.logger, enable_dynamic_routes=False) - self.clients: set[WebsocketClientHandler] = set() - self.manifest.name = "Web Server (frontend and api)" - self.manifest.description = ( - "The built-in webserver that hosts the Music Assistant Websockets API and frontend" - ) - self.manifest.icon = "web-box" - - @property - def base_url(self) -> str: - """Return the base_url for the streamserver.""" - return self._server.base_url - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - default_publish_ip = await get_ip() - if self.mass.running_as_hass_addon: - return ( - ConfigEntry( - key=CONF_EXPOSE_SERVER, - type=ConfigEntryType.BOOLEAN, - # hardcoded/static value - default_value=False, - label="Expose the webserver (port 8095)", - description="By default the Music Assistant webserver " - "(serving the API and frontend), runs on a protected internal network only " - "and you can securely access the webinterface using " - "Home Assistant's ingress service from the sidebar menu.\n\n" - "By enabling this option you also allow direct access to the webserver " - "from your local network, meaning you can navigate to " - f"http://{default_publish_ip}:8095 to access the webinterface. \n\n" - "Use this option on your own risk and never expose this port " - "directly to the internet.", - ), - ) - - # HA supervisor not present: user is responsible for securing the webserver - # we give the tools to do so by presenting config options - all_ips = await get_ips() - default_base_url = f"http://{default_publish_ip}:{DEFAULT_SERVER_PORT}" - return ( - ConfigEntry( - key=CONF_BASE_URL, - type=ConfigEntryType.STRING, - default_value=default_base_url, - label="Base URL", - description="The (base) URL to reach this webserver in the network. \n" - "Override this in advanced scenarios where for example you're running " - "the webserver behind a reverse proxy.", - ), - ConfigEntry( - key=CONF_BIND_PORT, - type=ConfigEntryType.INTEGER, - default_value=DEFAULT_SERVER_PORT, - label="TCP Port", - description="The TCP port to run the webserver.", - ), - ConfigEntry( - key=CONF_BIND_IP, - type=ConfigEntryType.STRING, - default_value="0.0.0.0", - options=(ConfigValueOption(x, x) for x in {"0.0.0.0", *all_ips}), - label="Bind to IP/interface", - description="Start the (web)server on this specific interface. \n" - "Use 0.0.0.0 to bind to all interfaces. \n" - "Set this address for example to a docker-internal network, " - "to enhance security and protect outside access to the webinterface and API. \n\n" - "This is an advanced setting that should normally " - "not be adjusted in regular setups.", - category="advanced", - ), - ) - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - # work out all routes - routes: list[tuple[str, str, Awaitable]] = [] - # frontend routes - frontend_dir = locate_frontend() - for filename in next(os.walk(frontend_dir))[2]: - if filename.endswith(".py"): - continue - filepath = os.path.join(frontend_dir, filename) - handler = partial(self._server.serve_static, filepath) - routes.append(("GET", f"/{filename}", handler)) - # add index - index_path = os.path.join(frontend_dir, "index.html") - handler = partial(self._server.serve_static, index_path) - routes.append(("GET", "/", handler)) - # add info - routes.append(("GET", "/info", self._handle_server_info)) - # add logging - routes.append(("GET", "/music-assistant.log", self._handle_application_log)) - # add websocket api - routes.append(("GET", "/ws", self._handle_ws_client)) - # also host the image proxy on the webserver - routes.append(("GET", "/imageproxy", self.mass.metadata.handle_imageproxy)) - # also host the audio preview service - routes.append(("GET", "/preview", self.serve_preview_stream)) - # start the webserver - default_publish_ip = await get_ip() - if self.mass.running_as_hass_addon: - # if we're running on the HA supervisor the webserver is secured by HA ingress - # we only start the webserver on the internal docker network and ingress connects - # to that internally and exposes the webUI securely - # if a user also wants to expose a the webserver non securely on his internal - # network he/she should explicitly do so (and know the risks) - self.publish_port = DEFAULT_SERVER_PORT - if config.get_value(CONF_EXPOSE_SERVER): - bind_ip = "0.0.0.0" - self.publish_ip = default_publish_ip - else: - # use internal ("172.30.32.) IP - self.publish_ip = bind_ip = next( - (x for x in await get_ips() if x.startswith("172.30.32.")), default_publish_ip - ) - base_url = f"http://{self.publish_ip}:{self.publish_port}" - else: - base_url = config.get_value(CONF_BASE_URL) - self.publish_port = config.get_value(CONF_BIND_PORT) - self.publish_ip = default_publish_ip - bind_ip = config.get_value(CONF_BIND_IP) - await self._server.setup( - bind_ip=bind_ip, - bind_port=self.publish_port, - base_url=base_url, - static_routes=routes, - # add assets subdir as static_content - static_content=("/assets", os.path.join(frontend_dir, "assets"), "assets"), - ) - - async def close(self) -> None: - """Cleanup on exit.""" - for client in set(self.clients): - await client.disconnect() - await self._server.close() - - async def serve_preview_stream(self, request: web.Request): - """Serve short preview sample.""" - provider_instance_id_or_domain = request.query["provider"] - item_id = urllib.parse.unquote(request.query["item_id"]) - resp = web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "audio/aac"}) - await resp.prepare(request) - async for chunk in get_preview_stream(self.mass, provider_instance_id_or_domain, item_id): - await resp.write(chunk) - return resp - - async def _handle_server_info(self, request: web.Request) -> web.Response: - """Handle request for server info.""" - return web.json_response(self.mass.get_server_info().to_dict()) - - async def _handle_ws_client(self, request: web.Request) -> web.WebSocketResponse: - connection = WebsocketClientHandler(self, request) - if lang := request.headers.get("Accept-Language"): - self.mass.metadata.set_default_preferred_language(lang.split(",")[0]) - try: - self.clients.add(connection) - return await connection.handle_client() - finally: - self.clients.remove(connection) - - async def _handle_application_log(self, request: web.Request) -> web.Response: - """Handle request to get the application log.""" - log_data = await self.mass.get_application_log() - return web.Response(text=log_data, content_type="text/text") - - -class WebsocketClientHandler: - """Handle an active websocket client connection.""" - - def __init__(self, webserver: WebserverController, request: web.Request) -> None: - """Initialize an active connection.""" - self.mass = webserver.mass - self.request = request - self.wsock = web.WebSocketResponse(heartbeat=55) - self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) - self._handle_task: asyncio.Task | None = None - self._writer_task: asyncio.Task | None = None - self._logger = webserver.logger - - async def disconnect(self) -> None: - """Disconnect client.""" - self._cancel() - if self._writer_task is not None: - await self._writer_task - - async def handle_client(self) -> web.WebSocketResponse: - """Handle a websocket response.""" - # ruff: noqa: PLR0915 - request = self.request - wsock = self.wsock - try: - async with asyncio.timeout(10): - await wsock.prepare(request) - except TimeoutError: - self._logger.warning("Timeout preparing request from %s", request.remote) - return wsock - - self._logger.log(VERBOSE_LOG_LEVEL, "Connection from %s", request.remote) - self._handle_task = asyncio.current_task() - self._writer_task = asyncio.create_task(self._writer()) - - # send server(version) info when client connects - self._send_message(self.mass.get_server_info()) - - # forward all events to clients - def handle_event(event: MassEvent) -> None: - self._send_message(event) - - unsub_callback = self.mass.subscribe(handle_event) - - disconnect_warn = None - - try: - while not wsock.closed: - msg = await wsock.receive() - - if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): - break - - if msg.type != WSMsgType.TEXT: - disconnect_warn = "Received non-Text message." - break - - self._logger.log(VERBOSE_LOG_LEVEL, "Received: %s", msg.data) - - try: - command_msg = CommandMessage.from_json(msg.data) - except ValueError: - disconnect_warn = f"Received invalid JSON: {msg.data}" - break - - self._handle_command(command_msg) - - except asyncio.CancelledError: - self._logger.debug("Connection closed by client") - - except Exception: - self._logger.exception("Unexpected error inside websocket API") - - finally: - # Handle connection shutting down. - unsub_callback() - self._logger.log(VERBOSE_LOG_LEVEL, "Unsubscribed from events") - - try: - self._to_write.put_nowait(None) - # Make sure all error messages are written before closing - await self._writer_task - await wsock.close() - except asyncio.QueueFull: # can be raised by put_nowait - self._writer_task.cancel() - - finally: - if disconnect_warn is None: - self._logger.log(VERBOSE_LOG_LEVEL, "Disconnected") - else: - self._logger.warning("Disconnected: %s", disconnect_warn) - - return wsock - - def _handle_command(self, msg: CommandMessage) -> None: - """Handle an incoming command from the client.""" - self._logger.debug("Handling command %s", msg.command) - - # work out handler for the given path/command - handler = self.mass.command_handlers.get(msg.command) - - if handler is None: - self._send_message( - ErrorResultMessage( - msg.message_id, - InvalidCommand.error_code, - f"Invalid command: {msg.command}", - ) - ) - self._logger.warning("Invalid command: %s", msg.command) - return - - # schedule task to handle the command - asyncio.create_task(self._run_handler(handler, msg)) - - async def _run_handler(self, handler: APICommandHandler, msg: CommandMessage) -> None: - try: - args = parse_arguments(handler.signature, handler.type_hints, msg.args) - result = handler.target(**args) - if hasattr(result, "__anext__"): - # handle async generator (for really large listings) - iterator = result - result: list[Any] = [] - async for item in iterator: - result.append(item) - if len(result) >= 500: - self._send_message( - SuccessResultMessage(msg.message_id, result, partial=True) - ) - result = [] - elif asyncio.iscoroutine(result): - result = await result - self._send_message(SuccessResultMessage(msg.message_id, result)) - except Exception as err: - if self._logger.isEnabledFor(logging.DEBUG): - self._logger.exception("Error handling message: %s", msg) - else: - self._logger.error("Error handling message: %s: %s", msg.command, str(err)) - self._send_message( - ErrorResultMessage(msg.message_id, getattr(err, "error_code", 999), str(err)) - ) - - async def _writer(self) -> None: - """Write outgoing messages.""" - # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): - while not self.wsock.closed: - if (process := await self._to_write.get()) is None: - break - - if not isinstance(process, str): - message: str = process() - else: - message = process - self._logger.log(VERBOSE_LOG_LEVEL, "Writing: %s", message) - await self.wsock.send_str(message) - - def _send_message(self, message: MessageType) -> None: - """Send a message to the client. - - Closes connection if the client is not reading the messages. - - Async friendly. - """ - _message = message.to_json() - - try: - self._to_write.put_nowait(_message) - except asyncio.QueueFull: - self._logger.error("Client exceeded max pending messages: %s", MAX_PENDING_MSG) - - self._cancel() - - def _cancel(self) -> None: - """Cancel the connection.""" - if self._handle_task is not None: - self._handle_task.cancel() - if self._writer_task is not None: - self._writer_task.cancel() diff --git a/music_assistant/server/helpers/__init__.py b/music_assistant/server/helpers/__init__.py deleted file mode 100644 index ca681394..00000000 --- a/music_assistant/server/helpers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Various server-seecific utils/helpers.""" diff --git a/music_assistant/server/helpers/api.py b/music_assistant/server/helpers/api.py deleted file mode 100644 index ddc7f0cd..00000000 --- a/music_assistant/server/helpers/api.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Helpers for dealing with API's to interact with Music Assistant.""" - -from __future__ import annotations - -import inspect -import logging -from collections.abc import Callable, Coroutine -from dataclasses import MISSING, dataclass -from datetime import datetime -from enum import Enum -from types import NoneType, UnionType -from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints - -LOGGER = logging.getLogger(__name__) - -_F = TypeVar("_F", bound=Callable[..., Any]) - - -@dataclass -class APICommandHandler: - """Model for an API command handler.""" - - command: str - signature: inspect.Signature - type_hints: dict[str, Any] - target: Callable[..., Coroutine[Any, Any, Any]] - - @classmethod - def parse( - cls, command: str, func: Callable[..., Coroutine[Any, Any, Any]] - ) -> APICommandHandler: - """Parse APICommandHandler by providing a function.""" - type_hints = get_type_hints(func) - # workaround for generic typevar ItemCls that needs to be resolved - # to the real media item type. TODO: find a better way to do this - # without this hack - for key, value in type_hints.items(): - if not hasattr(value, "__name__"): - continue - if value.__name__ == "ItemCls": - type_hints[key] = func.__self__.item_cls - return APICommandHandler( - command=command, - signature=inspect.signature(func), - type_hints=type_hints, - target=func, - ) - - -def api_command(command: str) -> Callable[[_F], _F]: - """Decorate a function as API route/command.""" - - def decorate(func: _F) -> _F: - func.api_cmd = command # type: ignore[attr-defined] - return func - - return decorate - - -def parse_arguments( - func_sig: inspect.Signature, - func_types: dict[str, Any], - args: dict | None, - strict: bool = False, -) -> dict[str, Any]: - """Parse (and convert) incoming arguments to correct types.""" - if args is None: - args = {} - final_args = {} - # ignore extra args if not strict - if strict: - for key, value in args.items(): - if key not in func_sig.parameters: - raise KeyError(f"Invalid parameter: '{key}'") - # parse arguments to correct type - for name, param in func_sig.parameters.items(): - value = args.get(name) - default = MISSING if param.default is inspect.Parameter.empty else param.default - final_args[name] = parse_value(name, value, func_types[name], default) - return final_args - - -def parse_utc_timestamp(datetime_string: str) -> datetime: - """Parse datetime from string.""" - return datetime.fromisoformat(datetime_string.replace("Z", "+00:00")) - - -def parse_value(name: str, value: Any, value_type: Any, default: Any = MISSING) -> Any: - """Try to parse a value from raw (json) data and type annotations.""" - if isinstance(value, dict) and hasattr(value_type, "from_dict"): - if "media_type" in value and value["media_type"] != value_type.media_type: - msg = "Invalid MediaType" - raise ValueError(msg) - return value_type.from_dict(value) - - if value is None and not isinstance(default, type(MISSING)): - return default - if value is None and value_type is NoneType: - return None - origin = get_origin(value_type) - if origin in (tuple, list): - return origin( - parse_value(name, subvalue, get_args(value_type)[0]) - for subvalue in value - if subvalue is not None - ) - if origin is dict: - subkey_type = get_args(value_type)[0] - subvalue_type = get_args(value_type)[1] - return { - parse_value(subkey, subkey, subkey_type): parse_value( - f"{subkey}.value", subvalue, subvalue_type - ) - for subkey, subvalue in value.items() - } - if origin is Union or origin is UnionType: - # try all possible types - sub_value_types = get_args(value_type) - for sub_arg_type in sub_value_types: - if value is NoneType and sub_arg_type is NoneType: - return value - # try them all until one succeeds - try: - return parse_value(name, value, sub_arg_type) - except (KeyError, TypeError, ValueError): - pass - # if we get to this point, all possibilities failed - # find out if we should raise or log this - err = ( - f"Value {value} of type {type(value)} is invalid for {name}, " - f"expected value of type {value_type}" - ) - if NoneType not in sub_value_types: - # raise exception, we have no idea how to handle this value - raise TypeError(err) - # failed to parse the (sub) value but None allowed, log only - logging.getLogger(__name__).warning(err) - return None - if origin is type: - return eval(value) - if value_type is Any: - return value - if value is None and value_type is not NoneType: - msg = f"`{name}` of type `{value_type}` is required." - raise KeyError(msg) - - try: - if issubclass(value_type, Enum): # type: ignore[arg-type] - return value_type(value) # type: ignore[operator] - if issubclass(value_type, datetime): # type: ignore[arg-type] - return parse_utc_timestamp(value) - except TypeError: - # happens if value_type is not a class - pass - - if value_type is float and isinstance(value, int): - return float(value) - if value_type is int and isinstance(value, str) and value.isnumeric(): - return int(value) - - if not isinstance(value, value_type): # type: ignore[arg-type] - msg = ( - f"Value {value} of type {type(value)} is invalid for {name}, " - f"expected value of type {value_type}" - ) - raise TypeError(msg) - return value diff --git a/music_assistant/server/helpers/app_vars.py b/music_assistant/server/helpers/app_vars.py deleted file mode 100644 index 9efefc60..00000000 --- a/music_assistant/server/helpers/app_vars.py +++ /dev/null @@ -1,5 +0,0 @@ -# pylint: skip-file -# fmt: off -# flake8: noqa -# ruff: noqa -(lambda __g: [(lambda __mod: [[[None for __g['app_var'], app_var.__name__ in [(lambda index: (lambda __l: [[AV(aap(__l['var'].encode()).decode()) for __l['var'] in [(vars.split('acb2')[__l['index']][::(-1)])]][0] for __l['index'] in [(index)]][0])({}), 'app_var')]][0] for __g['vars'] in [('3YTNyUDOyQTOacb2=EmN5M2YjdzMhljYzYzYhlDMmFGNlVTOmNDZwMzNxYzNacb2=UDMzEGOyADO1QWO5kDNygTMlJGN5QzNzIWOmZTOiVmMacb2yMTNzITNacb2=UDZhJmMldTZ3QTY4IjZ3kTNxYjN0czNwI2YxkTM5MjNacb2==QMh5WOmZnewM2d4UDblRzZacb20QzMwAjNacb2=QzNiRTO3EjMjFzMldjY3QTMwEDMwADMiNWZ5UWO3UWMacb2RJ1UJpXUPlzdvZUZ0w2VzVjMq1mblRnZvBHZ4x2RMZWbqhVS5JkdQ1WS38FVDpFTw9WcthGb41GaoV3dQV1QHNVRutUMjRFe09VeGh1RO1yQFtkZ3RnL5IERNJTVE5EerpWTzUkaPlWUYlFcKNET3FkaOlXSU9EMRpnT49maJdHaYpVa3lWS0MXVUpXSU5URWRlT1kUaPlWTzMGcKlXZuElZpFVMWtkSp9UaBhVZwo0QMl2Yq1UevtWVyEFbUNTWqlkNJNkWwRXbJNXSp5UMJpXVGpUaPl2YHJGaKlXZ')]][0] for __g['aap'] in [(__mod.b64decode)]][0])(__import__('base64', __g, __g, ('b64decode',), 0)) for __g['AV'] in [((lambda b, d: d.get('__metaclass__', getattr(b[0], '__class__', type(b[0])))('AV', b, d))((str,), (lambda __l: [__l for __l['__repr__'], __l['__repr__'].__name__ in [(lambda self: (lambda __l: [__name__ for __l['self'] in [(self)]][0])({}), '__repr__')]][0])({'__module__': __name__})))]][0])(globals()) diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py deleted file mode 100644 index 22dc1ef6..00000000 --- a/music_assistant/server/helpers/audio.py +++ /dev/null @@ -1,982 +0,0 @@ -"""Various helpers for audio streaming and manipulation.""" - -from __future__ import annotations - -import asyncio -import logging -import os -import re -import struct -import time -from collections.abc import AsyncGenerator -from io import BytesIO -from typing import TYPE_CHECKING - -import aiofiles -from aiohttp import ClientTimeout - -from music_assistant.common.helpers.global_cache import set_global_cache_values -from music_assistant.common.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads -from music_assistant.common.helpers.util import clean_stream_title -from music_assistant.common.models.config_entries import CoreConfig, PlayerConfig -from music_assistant.common.models.enums import MediaType, StreamType, VolumeNormalizationMode -from music_assistant.common.models.errors import ( - InvalidDataError, - MediaNotFoundError, - MusicAssistantError, - ProviderUnavailableError, -) -from music_assistant.common.models.media_items import AudioFormat, ContentType -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import ( - CONF_EQ_BASS, - CONF_EQ_MID, - CONF_EQ_TREBLE, - CONF_OUTPUT_CHANNELS, - CONF_VOLUME_NORMALIZATION, - CONF_VOLUME_NORMALIZATION_RADIO, - CONF_VOLUME_NORMALIZATION_TARGET, - CONF_VOLUME_NORMALIZATION_TRACKS, - MASS_LOGGER_NAME, - VERBOSE_LOG_LEVEL, -) - -from .ffmpeg import FFMpeg, get_ffmpeg_stream -from .playlists import IsHLSPlaylist, PlaylistItem, fetch_playlist, parse_m3u -from .process import AsyncProcess, check_output, communicate -from .throttle_retry import BYPASS_THROTTLER -from .util import TimedAsyncGenerator, create_tempfile, detect_charset - -if TYPE_CHECKING: - from music_assistant.common.models.player_queue import QueueItem - from music_assistant.server import MusicAssistant - -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.audio") - -# ruff: noqa: PLR0915 - -HTTP_HEADERS = {"User-Agent": "Lavf/60.16.100.MusicAssistant"} -HTTP_HEADERS_ICY = {**HTTP_HEADERS, "Icy-MetaData": "1"} - - -async def crossfade_pcm_parts( - fade_in_part: bytes, - fade_out_part: bytes, - pcm_format: AudioFormat, -) -> bytes: - """Crossfade two chunks of pcm/raw audio using ffmpeg.""" - sample_size = pcm_format.pcm_sample_size - # calculate the fade_length from the smallest chunk - fade_length = min(len(fade_in_part), len(fade_out_part)) / sample_size - fadeoutfile = create_tempfile() - async with aiofiles.open(fadeoutfile.name, "wb") as outfile: - await outfile.write(fade_out_part) - args = [ - # generic args - "ffmpeg", - "-hide_banner", - "-loglevel", - "quiet", - # fadeout part (as file) - "-acodec", - pcm_format.content_type.name.lower(), - "-f", - pcm_format.content_type.value, - "-ac", - str(pcm_format.channels), - "-ar", - str(pcm_format.sample_rate), - "-i", - fadeoutfile.name, - # fade_in part (stdin) - "-acodec", - pcm_format.content_type.name.lower(), - "-f", - pcm_format.content_type.value, - "-ac", - str(pcm_format.channels), - "-ar", - str(pcm_format.sample_rate), - "-i", - "-", - # filter args - "-filter_complex", - f"[0][1]acrossfade=d={fade_length}", - # output args - "-f", - pcm_format.content_type.value, - "-", - ] - _returncode, crossfaded_audio, _stderr = await communicate(args, fade_in_part) - if crossfaded_audio: - LOGGER.log( - VERBOSE_LOG_LEVEL, - "crossfaded 2 pcm chunks. fade_in_part: %s - " - "fade_out_part: %s - fade_length: %s seconds", - len(fade_in_part), - len(fade_out_part), - fade_length, - ) - return crossfaded_audio - # no crossfade_data, return original data instead - LOGGER.debug( - "crossfade of pcm chunks failed: not enough data? " "fade_in_part: %s - fade_out_part: %s", - len(fade_in_part), - len(fade_out_part), - ) - return fade_out_part + fade_in_part - - -async def strip_silence( - mass: MusicAssistant, # noqa: ARG001 - audio_data: bytes, - pcm_format: AudioFormat, - reverse: bool = False, -) -> bytes: - """Strip silence from begin or end of pcm audio using ffmpeg.""" - args = ["ffmpeg", "-hide_banner", "-loglevel", "quiet"] - args += [ - "-acodec", - pcm_format.content_type.name.lower(), - "-f", - pcm_format.content_type.value, - "-ac", - str(pcm_format.channels), - "-ar", - str(pcm_format.sample_rate), - "-i", - "-", - ] - # filter args - if reverse: - args += [ - "-af", - "areverse,atrim=start=0.2,silenceremove=start_periods=1:start_silence=0.1:start_threshold=0.02,areverse", - ] - else: - args += [ - "-af", - "atrim=start=0.2,silenceremove=start_periods=1:start_silence=0.1:start_threshold=0.02", - ] - # output args - args += ["-f", pcm_format.content_type.value, "-"] - _returncode, stripped_data, _stderr = await communicate(args, audio_data) - - # return stripped audio - bytes_stripped = len(audio_data) - len(stripped_data) - if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL): - seconds_stripped = round(bytes_stripped / pcm_format.pcm_sample_size, 2) - location = "end" if reverse else "begin" - LOGGER.log( - VERBOSE_LOG_LEVEL, - "stripped %s seconds of silence from %s of pcm audio. bytes stripped: %s", - seconds_stripped, - location, - bytes_stripped, - ) - return stripped_data - - -async def get_stream_details( - mass: MusicAssistant, - queue_item: QueueItem, - seek_position: int = 0, - fade_in: bool = False, - prefer_album_loudness: bool = False, -) -> StreamDetails: - """Get streamdetails for the given QueueItem. - - This is called just-in-time when a PlayerQueue wants a MediaItem to be played. - Do not try to request streamdetails in advance as this is expiring data. - param media_item: The QueueItem for which to request the streamdetails for. - """ - time_start = time.time() - LOGGER.debug("Getting streamdetails for %s", queue_item.uri) - if seek_position and (queue_item.media_type == MediaType.RADIO or not queue_item.duration): - LOGGER.warning("seeking is not possible on duration-less streams!") - seek_position = 0 - # we use a contextvar to bypass the throttler for this asyncio task/context - # this makes sure that playback has priority over other requests that may be - # happening in the background - BYPASS_THROTTLER.set(True) - if not queue_item.media_item: - # this should not happen, but guard it just in case - assert queue_item.streamdetails, "streamdetails required for non-mediaitem queueitems" - return queue_item.streamdetails - # always request the full library item as there might be other qualities available - media_item = ( - await mass.music.get_library_item_by_prov_id( - queue_item.media_item.media_type, - queue_item.media_item.item_id, - queue_item.media_item.provider, - ) - or queue_item.media_item - ) - # sort by quality and check track availability - for prov_media in sorted( - media_item.provider_mappings, key=lambda x: x.quality or 0, reverse=True - ): - if not prov_media.available: - LOGGER.debug(f"Skipping unavailable {prov_media}") - continue - # guard that provider is available - music_prov = mass.get_provider(prov_media.provider_instance) - if not music_prov: - LOGGER.debug(f"Skipping {prov_media} - provider not available") - continue # provider not available ? - # get streamdetails from provider - try: - streamdetails: StreamDetails = await music_prov.get_stream_details(prov_media.item_id) - except MusicAssistantError as err: - LOGGER.warning(str(err)) - else: - break - else: - raise MediaNotFoundError( - f"Unable to retrieve streamdetails for {queue_item.name} ({queue_item.uri})" - ) - - # work out how to handle radio stream - if ( - streamdetails.stream_type in (StreamType.ICY, StreamType.HLS, StreamType.HTTP) - and streamdetails.media_type == MediaType.RADIO - ): - resolved_url, stream_type = await resolve_radio_stream(mass, streamdetails.path) - streamdetails.path = resolved_url - streamdetails.stream_type = stream_type - # set queue_id on the streamdetails so we know what is being streamed - streamdetails.queue_id = queue_item.queue_id - # handle skip/fade_in details - streamdetails.seek_position = seek_position - streamdetails.fade_in = fade_in - if not streamdetails.duration: - streamdetails.duration = queue_item.duration - - # handle volume normalization details - if result := await mass.music.get_loudness( - streamdetails.item_id, - streamdetails.provider, - media_type=queue_item.media_type, - ): - streamdetails.loudness, streamdetails.loudness_album = result - streamdetails.prefer_album_loudness = prefer_album_loudness - player_settings = await mass.config.get_player_config(streamdetails.queue_id) - core_config = await mass.config.get_core_config("streams") - streamdetails.volume_normalization_mode = _get_normalization_mode( - core_config, player_settings, streamdetails - ) - streamdetails.target_loudness = player_settings.get_value(CONF_VOLUME_NORMALIZATION_TARGET) - - process_time = int((time.time() - time_start) * 1000) - LOGGER.debug("retrieved streamdetails for %s in %s milliseconds", queue_item.uri, process_time) - return streamdetails - - -async def get_media_stream( - mass: MusicAssistant, - streamdetails: StreamDetails, - pcm_format: AudioFormat, - audio_source: AsyncGenerator[bytes, None] | str, - filter_params: list[str] | None = None, - extra_input_args: list[str] | None = None, -) -> AsyncGenerator[bytes, None]: - """Get PCM audio stream for given media details.""" - logger = LOGGER.getChild("media_stream") - logger.debug("start media stream for: %s", streamdetails.uri) - strip_silence_begin = streamdetails.strip_silence_begin - strip_silence_end = streamdetails.strip_silence_end - if streamdetails.fade_in: - filter_params.append("afade=type=in:start_time=0:duration=3") - strip_silence_begin = False - bytes_sent = 0 - chunk_number = 0 - buffer: bytes = b"" - finished = False - - ffmpeg_proc = FFMpeg( - audio_input=audio_source, - input_format=streamdetails.audio_format, - output_format=pcm_format, - filter_params=filter_params, - extra_input_args=extra_input_args, - collect_log_history=True, - ) - try: - await ffmpeg_proc.start() - async for chunk in TimedAsyncGenerator( - ffmpeg_proc.iter_chunked(pcm_format.pcm_sample_size), 300 - ): - # for radio streams we just yield all chunks directly - if streamdetails.media_type == MediaType.RADIO: - yield chunk - bytes_sent += len(chunk) - continue - - chunk_number += 1 - # determine buffer size dynamically - if chunk_number < 5 and strip_silence_begin: - req_buffer_size = int(pcm_format.pcm_sample_size * 4) - elif chunk_number > 30 and strip_silence_end: - req_buffer_size = int(pcm_format.pcm_sample_size * 8) - else: - req_buffer_size = int(pcm_format.pcm_sample_size * 2) - - # always append to buffer - buffer += chunk - del chunk - - if len(buffer) < req_buffer_size: - # buffer is not full enough, move on - continue - - if chunk_number == 5 and strip_silence_begin: - # strip silence from begin of audio - chunk = await strip_silence( # noqa: PLW2901 - mass, buffer, pcm_format=pcm_format - ) - bytes_sent += len(chunk) - yield chunk - buffer = b"" - continue - - #### OTHER: enough data in buffer, feed to output - while len(buffer) > req_buffer_size: - yield buffer[: pcm_format.pcm_sample_size] - bytes_sent += pcm_format.pcm_sample_size - buffer = buffer[pcm_format.pcm_sample_size :] - - # end of audio/track reached - if strip_silence_end and buffer: - # strip silence from end of audio - buffer = await strip_silence( - mass, - buffer, - pcm_format=pcm_format, - reverse=True, - ) - # send remaining bytes in buffer - bytes_sent += len(buffer) - yield buffer - del buffer - finished = True - - finally: - await ffmpeg_proc.close() - - if bytes_sent == 0: - # edge case: no audio data was sent - streamdetails.stream_error = True - seconds_streamed = 0 - logger.warning("Stream error on %s", streamdetails.uri) - else: - # try to determine how many seconds we've streamed - seconds_streamed = bytes_sent / pcm_format.pcm_sample_size if bytes_sent else 0 - logger.debug( - "stream %s (with code %s) for %s - seconds streamed: %s", - "finished" if finished else "aborted", - ffmpeg_proc.returncode, - streamdetails.uri, - seconds_streamed, - ) - - streamdetails.seconds_streamed = seconds_streamed - # store accurate duration - if finished and not streamdetails.seek_position and seconds_streamed: - streamdetails.duration = seconds_streamed - - # parse loudnorm data if we have that collected - if ( - streamdetails.loudness is None - and streamdetails.volume_normalization_mode != VolumeNormalizationMode.DISABLED - and (finished or (seconds_streamed >= 300)) - ): - # if dynamic volume normalization is enabled and the entire track is streamed - # the loudnorm filter will output the measuremeet in the log, - # so we can use those directly instead of analyzing the audio - if loudness_details := parse_loudnorm(" ".join(ffmpeg_proc.log_history)): - logger.debug( - "Loudness measurement for %s: %s dB", - streamdetails.uri, - loudness_details, - ) - streamdetails.loudness = loudness_details - mass.create_task( - mass.music.set_loudness( - streamdetails.item_id, - streamdetails.provider, - loudness_details, - media_type=streamdetails.media_type, - ) - ) - else: - # no data from loudnorm filter found, we need to analyze the audio - # add background task to start analyzing the audio - task_id = f"analyze_loudness_{streamdetails.uri}" - mass.create_task(analyze_loudness, mass, streamdetails, task_id=task_id) - - # mark item as played in db if finished or streamed for 30 seconds - # NOTE that this is not the actual played time but the buffered time - # the queue controller will update the actual played time when the item is played - if finished or seconds_streamed > 30: - mass.create_task( - mass.music.mark_item_played( - streamdetails.media_type, - streamdetails.item_id, - streamdetails.provider, - ) - ) - - -def create_wave_header(samplerate=44100, channels=2, bitspersample=16, duration=None): - """Generate a wave header from given params.""" - file = BytesIO() - - # Generate format chunk - format_chunk_spec = b"<4sLHHLLHH" - format_chunk = struct.pack( - format_chunk_spec, - b"fmt ", # Chunk id - 16, # Size of this chunk (excluding chunk id and this field) - 1, # Audio format, 1 for PCM - channels, # Number of channels - int(samplerate), # Samplerate, 44100, 48000, etc. - int(samplerate * channels * (bitspersample / 8)), # Byterate - int(channels * (bitspersample / 8)), # Blockalign - bitspersample, # 16 bits for two byte samples, etc. - ) - # Generate data chunk - # duration = 3600*6.7 - data_chunk_spec = b"<4sL" - if duration is None: - # use max value possible - datasize = 4254768000 # = 6,7 hours at 44100/16 - else: - # calculate from duration - numsamples = samplerate * duration - datasize = int(numsamples * channels * (bitspersample / 8)) - data_chunk = struct.pack( - data_chunk_spec, - b"data", # Chunk id - int(datasize), # Chunk size (excluding chunk id and this field) - ) - sum_items = [ - # "WAVE" string following size field - 4, - # "fmt " + chunk size field + chunk size - struct.calcsize(format_chunk_spec), - # Size of data chunk spec + data size - struct.calcsize(data_chunk_spec) + datasize, - ] - # Generate main header - all_chunks_size = int(sum(sum_items)) - main_header_spec = b"<4sL4s" - main_header = struct.pack(main_header_spec, b"RIFF", all_chunks_size, b"WAVE") - # Write all the contents in - file.write(main_header) - file.write(format_chunk) - file.write(data_chunk) - - # return file.getvalue(), all_chunks_size + 8 - return file.getvalue() - - -async def resolve_radio_stream(mass: MusicAssistant, url: str) -> tuple[str, StreamType]: - """ - Resolve a streaming radio URL. - - Unwraps any playlists if needed. - Determines if the stream supports ICY metadata. - - Returns tuple; - - unfolded URL as string - - StreamType to determine ICY (radio) or HLS stream. - """ - cache_base_key = "resolved_radio_info" - if cache := await mass.cache.get(url, base_key=cache_base_key): - return cache - stream_type = StreamType.HTTP - resolved_url = url - timeout = ClientTimeout(total=0, connect=10, sock_read=5) - try: - async with mass.http_session.get( - url, headers=HTTP_HEADERS_ICY, allow_redirects=True, timeout=timeout - ) as resp: - headers = resp.headers - resp.raise_for_status() - if not resp.headers: - raise InvalidDataError("no headers found") - if headers.get("icy-metaint") is not None: - stream_type = StreamType.ICY - if ( - url.endswith((".m3u", ".m3u8", ".pls")) - or ".m3u?" in url - or ".m3u8?" in url - or ".pls?" in url - or "audio/x-mpegurl" in headers.get("content-type") - or "audio/x-scpls" in headers.get("content-type", "") - ): - # url is playlist, we need to unfold it - try: - substreams = await fetch_playlist(mass, url) - if not any(x for x in substreams if x.length): - for line in substreams: - if not line.is_url: - continue - # unfold first url of playlist - return await resolve_radio_stream(mass, line.path) - raise InvalidDataError("No content found in playlist") - except IsHLSPlaylist: - stream_type = StreamType.HLS - - except Exception as err: - LOGGER.warning("Error while parsing radio URL %s: %s", url, err) - return (url, stream_type) - - result = (resolved_url, stream_type) - cache_expiration = 3600 * 3 - await mass.cache.set(url, result, expiration=cache_expiration, base_key=cache_base_key) - return result - - -async def get_icy_radio_stream( - mass: MusicAssistant, url: str, streamdetails: StreamDetails -) -> AsyncGenerator[bytes, None]: - """Get (radio) audio stream from HTTP, including ICY metadata retrieval.""" - timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) - LOGGER.debug("Start streaming radio with ICY metadata from url %s", url) - async with mass.http_session.get( - url, allow_redirects=True, headers=HTTP_HEADERS_ICY, timeout=timeout - ) as resp: - headers = resp.headers - meta_int = int(headers["icy-metaint"]) - while True: - try: - yield await resp.content.readexactly(meta_int) - meta_byte = await resp.content.readexactly(1) - if meta_byte == b"\x00": - continue - meta_length = ord(meta_byte) * 16 - meta_data = await resp.content.readexactly(meta_length) - except asyncio.exceptions.IncompleteReadError: - break - if not meta_data: - continue - meta_data = meta_data.rstrip(b"\0") - stream_title = re.search(rb"StreamTitle='([^']*)';", meta_data) - if not stream_title: - continue - try: - # in 99% of the cases the stream title is utf-8 encoded - stream_title = stream_title.group(1).decode("utf-8") - except UnicodeDecodeError: - # fallback to iso-8859-1 - stream_title = stream_title.group(1).decode("iso-8859-1", errors="replace") - cleaned_stream_title = clean_stream_title(stream_title) - if cleaned_stream_title != streamdetails.stream_title: - LOGGER.log(VERBOSE_LOG_LEVEL, "ICY Radio streamtitle original: %s", stream_title) - LOGGER.log( - VERBOSE_LOG_LEVEL, "ICY Radio streamtitle cleaned: %s", cleaned_stream_title - ) - streamdetails.stream_title = cleaned_stream_title - - -async def get_hls_substream( - mass: MusicAssistant, - url: str, -) -> PlaylistItem: - """Select the (highest quality) HLS substream for given HLS playlist/URL.""" - timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) - # fetch master playlist and select (best) child playlist - # https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-10 - async with mass.http_session.get( - url, allow_redirects=True, headers=HTTP_HEADERS, timeout=timeout - ) as resp: - resp.raise_for_status() - raw_data = await resp.read() - encoding = resp.charset or await detect_charset(raw_data) - master_m3u_data = raw_data.decode(encoding) - substreams = parse_m3u(master_m3u_data) - # There is a chance that we did not get a master playlist with subplaylists - # but just a single master/sub playlist with the actual audio stream(s) - # so we need to detect if the playlist child's contain audio streams or - # sub-playlists. - if any( - x - for x in substreams - if (x.length or x.path.endswith((".mp4", ".aac"))) - and not x.path.endswith((".m3u", ".m3u8")) - ): - return PlaylistItem(path=url, key=substreams[0].key) - # sort substreams on best quality (highest bandwidth) when available - if any(x for x in substreams if x.stream_info): - substreams.sort(key=lambda x: int(x.stream_info.get("BANDWIDTH", "0")), reverse=True) - substream = substreams[0] - if not substream.path.startswith("http"): - # path is relative, stitch it together - base_path = url.rsplit("/", 1)[0] - substream.path = base_path + "/" + substream.path - return substream - - -async def get_http_stream( - mass: MusicAssistant, - url: str, - streamdetails: StreamDetails, - seek_position: int = 0, -) -> AsyncGenerator[bytes, None]: - """Get audio stream from HTTP.""" - LOGGER.debug("Start HTTP stream for %s (seek_position %s)", streamdetails.uri, seek_position) - if seek_position: - assert streamdetails.duration, "Duration required for seek requests" - # try to get filesize with a head request - seek_supported = streamdetails.can_seek - if seek_position or not streamdetails.size: - async with mass.http_session.head(url, allow_redirects=True, headers=HTTP_HEADERS) as resp: - resp.raise_for_status() - if size := resp.headers.get("Content-Length"): - streamdetails.size = int(size) - seek_supported = resp.headers.get("Accept-Ranges") == "bytes" - # headers - headers = {**HTTP_HEADERS} - timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) - skip_bytes = 0 - if seek_position and streamdetails.size: - skip_bytes = int(streamdetails.size / streamdetails.duration * seek_position) - headers["Range"] = f"bytes={skip_bytes}-{streamdetails.size}" - - # seeking an unknown or container format is not supported due to the (moov) headers - if seek_position and ( - not seek_supported - or streamdetails.audio_format.content_type - in ( - ContentType.UNKNOWN, - ContentType.M4A, - ContentType.M4B, - ) - ): - LOGGER.warning( - "Seeking in %s (%s) not possible.", - streamdetails.uri, - streamdetails.audio_format.output_format_str, - ) - seek_position = 0 - streamdetails.seek_position = 0 - - # start the streaming from http - bytes_received = 0 - async with mass.http_session.get( - url, allow_redirects=True, headers=headers, timeout=timeout - ) as resp: - is_partial = resp.status == 206 - if seek_position and not is_partial: - raise InvalidDataError("HTTP source does not support seeking!") - resp.raise_for_status() - async for chunk in resp.content.iter_any(): - bytes_received += len(chunk) - yield chunk - - # store size on streamdetails for later use - if not streamdetails.size: - streamdetails.size = bytes_received - LOGGER.debug( - "Finished HTTP stream for %s (transferred %s/%s bytes)", - streamdetails.uri, - bytes_received, - streamdetails.size, - ) - - -async def get_file_stream( - mass: MusicAssistant, # noqa: ARG001 - filename: str, - streamdetails: StreamDetails, - seek_position: int = 0, -) -> AsyncGenerator[bytes, None]: - """Get audio stream from local accessible file.""" - if seek_position: - assert streamdetails.duration, "Duration required for seek requests" - if not streamdetails.size: - stat = await asyncio.to_thread(os.stat, filename) - streamdetails.size = stat.st_size - - # seeking an unknown or container format is not supported due to the (moov) headers - if seek_position and ( - streamdetails.audio_format.content_type - in ( - ContentType.UNKNOWN, - ContentType.M4A, - ContentType.M4B, - ContentType.MP4, - ) - ): - LOGGER.warning( - "Seeking in %s (%s) not possible.", - streamdetails.uri, - streamdetails.audio_format.output_format_str, - ) - seek_position = 0 - streamdetails.seek_position = 0 - - chunk_size = get_chunksize(streamdetails.audio_format) - async with aiofiles.open(streamdetails.data, "rb") as _file: - if seek_position: - seek_pos = int((streamdetails.size / streamdetails.duration) * seek_position) - await _file.seek(seek_pos) - # yield chunks of data from file - while True: - data = await _file.read(chunk_size) - if not data: - break - yield data - - -async def check_audio_support() -> tuple[bool, bool, str]: - """Check if ffmpeg is present (with/without libsoxr support).""" - # check for FFmpeg presence - returncode, output = await check_output("ffmpeg", "-version") - ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode() - - # use globals as in-memory cache - version = output.decode().split("ffmpeg version ")[1].split(" ")[0].split("-")[0] - libsoxr_support = "enable-libsoxr" in output.decode() - result = (ffmpeg_present, libsoxr_support, version) - # store in global cache for easy access by 'get_ffmpeg_args' - await set_global_cache_values({"ffmpeg_support": result}) - return result - - -async def get_preview_stream( - mass: MusicAssistant, - provider_instance_id_or_domain: str, - track_id: str, -) -> AsyncGenerator[bytes, None]: - """Create a 30 seconds preview audioclip for the given streamdetails.""" - if not (music_prov := mass.get_provider(provider_instance_id_or_domain)): - raise ProviderUnavailableError - streamdetails = await music_prov.get_stream_details(track_id) - async for chunk in get_ffmpeg_stream( - audio_input=music_prov.get_audio_stream(streamdetails, 30) - if streamdetails.stream_type == StreamType.CUSTOM - else streamdetails.path, - input_format=streamdetails.audio_format, - output_format=AudioFormat(content_type=ContentType.AAC), - extra_input_args=["-to", "30"], - ): - yield chunk - - -async def get_silence( - duration: int, - output_format: AudioFormat, -) -> AsyncGenerator[bytes, None]: - """Create stream of silence, encoded to format of choice.""" - if output_format.content_type.is_pcm(): - # pcm = just zeros - for _ in range(duration): - yield b"\0" * int(output_format.sample_rate * (output_format.bit_depth / 8) * 2) - return - if output_format.content_type == ContentType.WAV: - # wav silence = wave header + zero's - yield create_wave_header( - samplerate=output_format.sample_rate, - channels=2, - bitspersample=output_format.bit_depth, - duration=duration, - ) - for _ in range(duration): - yield b"\0" * int(output_format.sample_rate * (output_format.bit_depth / 8) * 2) - return - # use ffmpeg for all other encodings - args = [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "quiet", - "-f", - "lavfi", - "-i", - f"anullsrc=r={output_format.sample_rate}:cl={'stereo'}", - "-t", - str(duration), - "-f", - output_format.output_format_str, - "-", - ] - async with AsyncProcess(args, stdout=True) as ffmpeg_proc: - async for chunk in ffmpeg_proc.iter_chunked(): - yield chunk - - -def get_chunksize( - fmt: AudioFormat, - seconds: int = 1, -) -> int: - """Get a default chunk/file size for given contenttype in bytes.""" - pcm_size = int(fmt.sample_rate * (fmt.bit_depth / 8) * fmt.channels * seconds) - if fmt.content_type.is_pcm() or fmt.content_type == ContentType.WAV: - return pcm_size - if fmt.content_type in (ContentType.WAV, ContentType.AIFF, ContentType.DSF): - return pcm_size - if fmt.bit_rate: - return int(((fmt.bit_rate * 1000) / 8) * seconds) - if fmt.content_type in (ContentType.FLAC, ContentType.WAVPACK, ContentType.ALAC): - # assume 74.7% compression ratio (level 0) - # source: https://z-issue.com/wp/flac-compression-level-comparison/ - return int(pcm_size * 0.747) - if fmt.content_type in (ContentType.MP3, ContentType.OGG): - return int((320000 / 8) * seconds) - if fmt.content_type in (ContentType.AAC, ContentType.M4A): - return int((256000 / 8) * seconds) - return int((320000 / 8) * seconds) - - -def get_player_filter_params( - mass: MusicAssistant, - player_id: str, -) -> list[str]: - """Get player specific filter parameters for ffmpeg (if any).""" - # collect all players-specific filter args - # TODO: add convolution/DSP/roomcorrections here?! - filter_params = [] - - # the below is a very basic 3-band equalizer, - # this could be a lot more sophisticated at some point - if (eq_bass := mass.config.get_raw_player_config_value(player_id, CONF_EQ_BASS, 0)) != 0: - filter_params.append(f"equalizer=frequency=100:width=200:width_type=h:gain={eq_bass}") - if (eq_mid := mass.config.get_raw_player_config_value(player_id, CONF_EQ_MID, 0)) != 0: - filter_params.append(f"equalizer=frequency=900:width=1800:width_type=h:gain={eq_mid}") - if (eq_treble := mass.config.get_raw_player_config_value(player_id, CONF_EQ_TREBLE, 0)) != 0: - filter_params.append(f"equalizer=frequency=9000:width=18000:width_type=h:gain={eq_treble}") - # handle output mixing only left or right - conf_channels = mass.config.get_raw_player_config_value( - player_id, CONF_OUTPUT_CHANNELS, "stereo" - ) - if conf_channels == "left": - filter_params.append("pan=mono|c0=FL") - elif conf_channels == "right": - filter_params.append("pan=mono|c0=FR") - - # add a peak limiter at the end of the filter chain - filter_params.append("alimiter=limit=-2dB:level=false:asc=true") - - return filter_params - - -def parse_loudnorm(raw_stderr: bytes | str) -> float | None: - """Parse Loudness measurement from ffmpeg stderr output.""" - stderr_data = raw_stderr.decode() if isinstance(raw_stderr, bytes) else raw_stderr - if "[Parsed_loudnorm_" not in stderr_data: - return None - stderr_data = stderr_data.split("[Parsed_loudnorm_")[1] - stderr_data = "{" + stderr_data.rsplit("{")[-1].strip() - stderr_data = stderr_data.rsplit("}")[0].strip() + "}" - try: - loudness_data = json_loads(stderr_data) - except JSON_DECODE_EXCEPTIONS: - return None - return float(loudness_data["input_i"]) - - -async def analyze_loudness( - mass: MusicAssistant, - streamdetails: StreamDetails, -) -> None: - """Analyze media item's audio, to calculate EBU R128 loudness.""" - if result := await mass.music.get_loudness( - streamdetails.item_id, - streamdetails.provider, - media_type=streamdetails.media_type, - ): - # only when needed we do the analyze job - streamdetails.loudness = result - return - - logger = LOGGER.getChild("analyze_loudness") - logger.debug("Start analyzing audio for %s", streamdetails.uri) - - extra_input_args = [ - # limit to 10 minutes to reading too much in memory - "-t", - "600", - ] - if streamdetails.stream_type == StreamType.CUSTOM: - audio_source = mass.get_provider(streamdetails.provider).get_audio_stream( - streamdetails, - ) - elif streamdetails.stream_type == StreamType.HLS: - substream = await get_hls_substream(mass, streamdetails.path) - audio_source = substream.path - elif streamdetails.stream_type == StreamType.ENCRYPTED_HTTP: - audio_source = streamdetails.path - extra_input_args += ["-decryption_key", streamdetails.decryption_key] - else: - audio_source = streamdetails.path - - # calculate BS.1770 R128 integrated loudness with ffmpeg - async with FFMpeg( - audio_input=audio_source, - input_format=streamdetails.audio_format, - output_format=streamdetails.audio_format, - audio_output="NULL", - filter_params=["ebur128=framelog=verbose"], - extra_input_args=extra_input_args, - collect_log_history=True, - ) as ffmpeg_proc: - await ffmpeg_proc.wait() - log_lines = ffmpeg_proc.log_history - log_lines_str = "\n".join(log_lines) - try: - loudness_str = ( - log_lines_str.split("Integrated loudness")[1].split("I:")[1].split("LUFS")[0] - ) - loudness = float(loudness_str.strip()) - except (IndexError, ValueError, AttributeError): - LOGGER.warning( - "Could not determine integrated loudness of %s - %s", - streamdetails.uri, - log_lines_str or "received empty value", - ) - else: - streamdetails.loudness = loudness - await mass.music.set_loudness( - streamdetails.item_id, - streamdetails.provider, - loudness, - media_type=streamdetails.media_type, - ) - logger.debug( - "Integrated loudness of %s is: %s", - streamdetails.uri, - loudness, - ) - - -def _get_normalization_mode( - core_config: CoreConfig, player_config: PlayerConfig, streamdetails: StreamDetails -) -> VolumeNormalizationMode: - if not player_config.get_value(CONF_VOLUME_NORMALIZATION): - # disabled for this player - return VolumeNormalizationMode.DISABLED - # work out preference for track or radio - preference = VolumeNormalizationMode( - core_config.get_value( - CONF_VOLUME_NORMALIZATION_RADIO - if streamdetails.media_type == MediaType.RADIO - else CONF_VOLUME_NORMALIZATION_TRACKS, - ) - ) - - # handle no measurement available but fallback to dynamic mode is allowed - if streamdetails.loudness is None and preference == VolumeNormalizationMode.FALLBACK_DYNAMIC: - return VolumeNormalizationMode.DYNAMIC - - # handle no measurement available and no fallback allowed - if streamdetails.loudness is None and preference == VolumeNormalizationMode.MEASUREMENT_ONLY: - return VolumeNormalizationMode.DISABLED - - # handle no measurement available and fallback to fixed gain is allowed - if streamdetails.loudness is None and preference == VolumeNormalizationMode.FALLBACK_FIXED_GAIN: - return VolumeNormalizationMode.FIXED_GAIN - - # simply return the preference - return preference diff --git a/music_assistant/server/helpers/auth.py b/music_assistant/server/helpers/auth.py deleted file mode 100644 index 76c47741..00000000 --- a/music_assistant/server/helpers/auth.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Helper(s) to deal with authentication for (music) providers.""" - -from __future__ import annotations - -import asyncio -from types import TracebackType -from typing import TYPE_CHECKING - -from aiohttp.web import Request, Response - -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.errors import LoginFailed - -if TYPE_CHECKING: - from music_assistant.server import MusicAssistant - - -class AuthenticationHelper: - """Context manager helper class for authentication with a forward and redirect URL.""" - - def __init__(self, mass: MusicAssistant, session_id: str) -> None: - """ - Initialize the Authentication Helper. - - Params: - - url: The URL the user needs to open for authentication. - - session_id: a unique id for this auth session. - """ - self.mass = mass - self.session_id = session_id - self._callback_response: asyncio.Queue[dict[str, str]] = asyncio.Queue(1) - - @property - def callback_url(self) -> str: - """Return the callback URL.""" - return f"{self.mass.streams.base_url}/callback/{self.session_id}" - - async def __aenter__(self) -> AuthenticationHelper: - """Enter context manager.""" - self.mass.streams.register_dynamic_route( - f"/callback/{self.session_id}", self._handle_callback, "GET" - ) - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - self.mass.streams.unregister_dynamic_route(f"/callback/{self.session_id}", "GET") - - async def authenticate(self, auth_url: str, timeout: int = 60) -> dict[str, str]: - """Start the auth process and return any query params if received on the callback.""" - self.send_url(auth_url) - return await self.wait_for_callback(timeout) - - def send_url(self, auth_url: str) -> None: - """Send the user to the given URL to authenticate (or fill in a code).""" - # redirect the user in the frontend to the auth url - self.mass.signal_event(EventType.AUTH_SESSION, self.session_id, auth_url) - - async def wait_for_callback(self, timeout: int = 60) -> dict[str, str]: - """Wait for the external party to call the callback and return any query strings.""" - try: - async with asyncio.timeout(timeout): - return await self._callback_response.get() - except TimeoutError as err: - raise LoginFailed("Timeout while waiting for authentication callback") from err - - async def _handle_callback(self, request: Request) -> Response: - """Handle callback response.""" - params = dict(request.query) - await self._callback_response.put(params) - return_html = """ - - - Authentication completed, you may now close this window. - - - """ - return Response(body=return_html, headers={"content-type": "text/html"}) diff --git a/music_assistant/server/helpers/compare.py b/music_assistant/server/helpers/compare.py deleted file mode 100644 index 808b319f..00000000 --- a/music_assistant/server/helpers/compare.py +++ /dev/null @@ -1,475 +0,0 @@ -"""Several helper/utils to compare objects.""" - -from __future__ import annotations - -import re -from difflib import SequenceMatcher - -import unidecode - -from music_assistant.common.models.enums import ExternalID, MediaType -from music_assistant.common.models.media_items import ( - Album, - Artist, - ItemMapping, - MediaItem, - MediaItemMetadata, - MediaItemType, - Playlist, - Radio, - Track, -) - -IGNORE_VERSIONS = ( - "explicit", # explicit is matched separately - "music from and inspired by the motion picture", - "original soundtrack", - "hi-res", # quality is handled separately -) - - -def compare_media_item( - base_item: MediaItemType | ItemMapping, - compare_item: MediaItemType | ItemMapping, - strict: bool = True, -) -> bool | None: - """Compare two media items and return True if they match.""" - if base_item.media_type == MediaType.ARTIST and compare_item.media_type == MediaType.ARTIST: - return compare_artist(base_item, compare_item, strict) - if base_item.media_type == MediaType.ALBUM and compare_item.media_type == MediaType.ALBUM: - return compare_album(base_item, compare_item, strict) - if base_item.media_type == MediaType.TRACK and compare_item.media_type == MediaType.TRACK: - return compare_track(base_item, compare_item, strict) - if base_item.media_type == MediaType.PLAYLIST and compare_item.media_type == MediaType.PLAYLIST: - return compare_playlist(base_item, compare_item, strict) - if base_item.media_type == MediaType.RADIO and compare_item.media_type == MediaType.RADIO: - return compare_radio(base_item, compare_item, strict) - return compare_item_mapping(base_item, compare_item, strict) - - -def compare_artist( - base_item: Artist | ItemMapping, - compare_item: Artist | ItemMapping, - strict: bool = True, -) -> bool | None: - """Compare two artist items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # return early on exact item_id match - if compare_item_ids(base_item, compare_item): - return True - # return early on (un)matched external id - for ext_id in (ExternalID.DISCOGS, ExternalID.MB_ARTIST, ExternalID.TADB): - external_id_match = compare_external_ids( - base_item.external_ids, compare_item.external_ids, ext_id - ) - if external_id_match is not None: - return external_id_match - # finally comparing on (exact) name match - return compare_strings(base_item.name, compare_item.name, strict=strict) - - -def compare_album( - base_item: Album | ItemMapping | None, - compare_item: Album | ItemMapping | None, - strict: bool = True, -) -> bool | None: - """Compare two album items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # return early on exact item_id match - if compare_item_ids(base_item, compare_item): - return True - - # return early on (un)matched external id - for ext_id in ( - ExternalID.DISCOGS, - ExternalID.MB_ALBUM, - ExternalID.TADB, - ExternalID.ASIN, - ExternalID.BARCODE, - ): - external_id_match = compare_external_ids( - base_item.external_ids, compare_item.external_ids, ext_id - ) - if external_id_match is not None: - return external_id_match - - # compare version - if not compare_version(base_item.version, compare_item.version): - return False - # compare name - if not compare_strings(base_item.name, compare_item.name, strict=True): - return False - if not strict and (isinstance(base_item, ItemMapping) or isinstance(compare_item, ItemMapping)): - return True - # for strict matching we REQUIRE both items to be a real album object - assert isinstance(base_item, Album) - assert isinstance(compare_item, Album) - # compare year - if base_item.year and compare_item.year and base_item.year != compare_item.year: - return False - # compare explicitness - if compare_explicit(base_item.metadata, compare_item.metadata) is False: - return False - # compare album artist(s) - return compare_artists(base_item.artists, compare_item.artists, not strict) - - -def compare_track( - base_item: Track | None, - compare_item: Track | None, - strict: bool = True, - track_albums: list[Album] | None = None, -) -> bool: - """Compare two track items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # return early on exact item_id match - if compare_item_ids(base_item, compare_item): - return True - # return early on (un)matched primary/unique external id - for ext_id in ( - ExternalID.MB_RECORDING, - ExternalID.MB_TRACK, - ExternalID.ACOUSTID, - ): - external_id_match = compare_external_ids( - base_item.external_ids, compare_item.external_ids, ext_id - ) - if external_id_match is not None: - return external_id_match - # check secondary external id matches - for ext_id in ( - ExternalID.DISCOGS, - ExternalID.TADB, - ExternalID.ISRC, - ExternalID.ASIN, - ): - external_id_match = compare_external_ids( - base_item.external_ids, compare_item.external_ids, ext_id - ) - if external_id_match is True: - # we got a 'soft-match' on a secondary external id (like ISRC) - # but we do a double check on duration - if abs(base_item.duration - compare_item.duration) <= 2: - return True - - # compare name - if not compare_strings(base_item.name, compare_item.name, strict=True): - return False - # track artist(s) must match - if not compare_artists(base_item.artists, compare_item.artists, any_match=not strict): - return False - # track version must match - if strict and not compare_version(base_item.version, compare_item.version): - return False - # check if both tracks are (not) explicit - if base_item.metadata.explicit is None and isinstance(base_item.album, Album): - base_item.metadata.explicit = base_item.album.metadata.explicit - if compare_item.metadata.explicit is None and isinstance(compare_item.album, Album): - compare_item.metadata.explicit = compare_item.album.metadata.explicit - if strict and compare_explicit(base_item.metadata, compare_item.metadata) is False: - return False - - # exact albumtrack match = 100% match - if ( - base_item.album - and compare_item.album - and compare_album(base_item.album, compare_item.album, False) - and base_item.disc_number - and compare_item.disc_number - and base_item.track_number - and compare_item.track_number - and base_item.disc_number == compare_item.disc_number - and base_item.track_number == compare_item.track_number - ): - return True - - # fallback: exact album match and (near-exact) track duration match - if ( - base_item.album is not None - and compare_item.album is not None - and (base_item.track_number == 0 or compare_item.track_number == 0) - and compare_album(base_item.album, compare_item.album, False) - and abs(base_item.duration - compare_item.duration) <= 3 - ): - return True - - # fallback: additional compare albums provided for base track - if ( - compare_item.album is not None - and track_albums - and abs(base_item.duration - compare_item.duration) <= 3 - ): - for track_album in track_albums: - if compare_album(track_album, compare_item.album, False): - return True - - # fallback edge case: albumless track with same duration - if ( - base_item.album is None - and compare_item.album is None - and base_item.disc_number == 0 - and compare_item.disc_number == 0 - and base_item.track_number == 0 - and compare_item.track_number == 0 - and base_item.duration == compare_item.duration - ): - return True - - if strict: - # in strict mode, we require an exact album match so return False here - return False - - # Accept last resort (in non strict mode): (near) exact duration, - # otherwise fail all other cases. - # Note that as this stage, all other info already matches, - # such as title, artist etc. - return abs(base_item.duration - compare_item.duration) <= 2 - - -def compare_playlist( - base_item: Playlist | ItemMapping, - compare_item: Playlist | ItemMapping, - strict: bool = True, -) -> bool | None: - """Compare two Playlist items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # require (exact) name match - if not compare_strings(base_item.name, compare_item.name, strict=strict): - return False - # require exact owner match (if not ItemMapping) - if isinstance(base_item, Playlist) and isinstance(compare_item, Playlist): - if not compare_strings(base_item.owner, compare_item.owner): - return False - # a playlist is always unique - so do a strict compare on item id(s) - return compare_item_ids(base_item, compare_item) - - -def compare_radio( - base_item: Radio | ItemMapping, - compare_item: Radio | ItemMapping, - strict: bool = True, -) -> bool | None: - """Compare two Radio items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # return early on exact item_id match - if compare_item_ids(base_item, compare_item): - return True - # compare version - if not compare_version(base_item.version, compare_item.version): - return False - # finally comparing on (exact) name match - return compare_strings(base_item.name, compare_item.name, strict=strict) - - -def compare_item_mapping( - base_item: ItemMapping, - compare_item: ItemMapping, - strict: bool = True, -) -> bool | None: - """Compare two ItemMapping items and return True if they match.""" - if base_item is None or compare_item is None: - return False - # return early on exact item_id match - if compare_item_ids(base_item, compare_item): - return True - # return early on (un)matched external id - external_id_match = compare_external_ids(base_item.external_ids, compare_item.external_ids) - if external_id_match is not None: - return external_id_match - # compare version - if not compare_version(base_item.version, compare_item.version): - return False - # finally comparing on (exact) name match - return compare_strings(base_item.name, compare_item.name, strict=strict) - - -def compare_artists( - base_items: list[Artist | ItemMapping], - compare_items: list[Artist | ItemMapping], - any_match: bool = True, -) -> bool: - """Compare two lists of artist and return True if both lists match (exactly).""" - if not base_items or not compare_items: - return False - # match if first artist matches in both lists - if compare_artist(base_items[0], compare_items[0]): - return True - # compare the artist lists - matches = 0 - for base_item in base_items: - for compare_item in compare_items: - if compare_artist(base_item, compare_item): - if any_match: - return True - matches += 1 - return len(base_items) == len(compare_items) == matches - - -def compare_albums( - base_items: list[Album | ItemMapping], - compare_items: list[Album | ItemMapping], - any_match: bool = True, -) -> bool: - """Compare two lists of albums and return True if a match was found.""" - matches = 0 - for base_item in base_items: - for compare_item in compare_items: - if compare_album(base_item, compare_item): - if any_match: - return True - matches += 1 - return len(base_items) == matches - - -def compare_item_ids( - base_item: MediaItem | ItemMapping, compare_item: MediaItem | ItemMapping -) -> bool: - """Compare item_id(s) of two media items.""" - if not base_item.provider or not compare_item.provider: - return False - if not base_item.item_id or not compare_item.item_id: - return False - if base_item.provider == compare_item.provider and base_item.item_id == compare_item.item_id: - return True - - base_prov_ids = getattr(base_item, "provider_mappings", None) - compare_prov_ids = getattr(compare_item, "provider_mappings", None) - - if base_prov_ids is not None: - for prov_l in base_item.provider_mappings: - if ( - prov_l.provider_domain == compare_item.provider - and prov_l.item_id == compare_item.item_id - ): - return True - - if compare_prov_ids is not None: - for prov_r in compare_item.provider_mappings: - if prov_r.provider_domain == base_item.provider and prov_r.item_id == base_item.item_id: - return True - - if base_prov_ids is not None and compare_prov_ids is not None: - for prov_l in base_item.provider_mappings: - for prov_r in compare_item.provider_mappings: - if prov_l.provider_domain != prov_r.provider_domain: - continue - if prov_l.item_id == prov_r.item_id: - return True - return False - - -def compare_external_ids( - external_ids_base: set[tuple[ExternalID, str]], - external_ids_compare: set[tuple[ExternalID, str]], - external_id_type: ExternalID, -) -> bool | None: - """Compare external ids and return True if a match was found.""" - base_ids = {x[1] for x in external_ids_base if x[0] == external_id_type} - if not base_ids: - # return early if the requested external id type is not present in the base set - return None - compare_ids = {x[1] for x in external_ids_compare if x[0] == external_id_type} - if not compare_ids: - # return early if the requested external id type is not present in the compare set - return None - for base_id in base_ids: - if base_id in compare_ids: - return True - # handle upc stored as EAN-13 barcode - if external_id_type == ExternalID.BARCODE and len(base_id) == 12: - if f"0{base_id}" in compare_ids: - return True - # handle EAN-13 stored as UPC barcode - if external_id_type == ExternalID.BARCODE and len(base_id) == 13: - if base_id[1:] in compare_ids: - return True - # return false if the identifier is unique (e.g. musicbrainz id) - if external_id_type.is_unique: - return False - return None - - -def create_safe_string(input_str: str, lowercase: bool = True, replace_space: bool = False) -> str: - """Return clean lowered string for compare actions.""" - input_str = input_str.lower().strip() if lowercase else input_str.strip() - unaccented_string = unidecode.unidecode(input_str) - regex = r"[^a-zA-Z0-9]" if replace_space else r"[^a-zA-Z0-9 ]" - return re.sub(regex, "", unaccented_string) - - -def loose_compare_strings(base: str, alt: str) -> bool: - """Compare strings and return True even on partial match.""" - # this is used to display 'versions' of the same track/album - # where we account for other spelling or some additional wording in the title - word_count = len(base.split(" ")) - if word_count == 1 and len(base) < 10: - return compare_strings(base, alt, False) - base_comp = create_safe_string(base) - alt_comp = create_safe_string(alt) - if base_comp in alt_comp: - return True - return alt_comp in base_comp - - -def compare_strings(str1: str, str2: str, strict: bool = True) -> bool: - """Compare strings and return True if we have an (almost) perfect match.""" - if not str1 or not str2: - return False - str1_lower = str1.lower() - str2_lower = str2.lower() - if strict: - return str1_lower == str2_lower - # return early if total length mismatch - if abs(len(str1) - len(str2)) > 4: - return False - # handle '&' vs 'And' - if " & " in str1_lower and " and " in str2_lower: - str2 = str2_lower.replace(" and ", " & ") - elif " and " in str1_lower and " & " in str2: - str2 = str2.replace(" & ", " and ") - if create_safe_string(str1) == create_safe_string(str2): - return True - # last resort: use difflib to compare strings - required_accuracy = 0.9 if (len(str1) + len(str2)) > 18 else 0.8 - return SequenceMatcher(a=str1_lower, b=str2).ratio() > required_accuracy - - -def compare_version(base_version: str, compare_version: str) -> bool: - """Compare version string.""" - if not base_version and not compare_version: - return True - if not base_version and compare_version.lower() in IGNORE_VERSIONS: - return True - if not compare_version and base_version.lower() in IGNORE_VERSIONS: - return True - if not base_version and compare_version: - return False - if base_version and not compare_version: - return False - - if " " not in base_version and " " not in compare_version: - return compare_strings(base_version, compare_version, False) - - # do this the hard way as sometimes the version string is in the wrong order - base_versions = sorted(base_version.lower().split(" ")) - compare_versions = sorted(compare_version.lower().split(" ")) - # filter out words we can ignore (such as 'version') - ignore_words = [*IGNORE_VERSIONS, "version", "edition", "variant", "versie", "versione"] - base_versions = [x for x in base_versions if x not in ignore_words] - compare_versions = [x for x in compare_versions if x not in ignore_words] - - return base_versions == compare_versions - - -def compare_explicit(base: MediaItemMetadata, compare: MediaItemMetadata) -> bool | None: - """Compare if explicit is same in metadata.""" - if base.explicit is not None and compare.explicit is not None: - # explicitness info is not always present in metadata - # only strict compare them if both have the info set - return base.explicit == compare.explicit - return None diff --git a/music_assistant/server/helpers/database.py b/music_assistant/server/helpers/database.py deleted file mode 100644 index 9e0e37e1..00000000 --- a/music_assistant/server/helpers/database.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Database helpers and logic.""" - -from __future__ import annotations - -import asyncio -import logging -import os -import time -from contextlib import asynccontextmanager -from sqlite3 import OperationalError -from typing import TYPE_CHECKING, Any - -import aiosqlite - -from music_assistant.constants import MASS_LOGGER_NAME - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Mapping - -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.database") - -ENABLE_DEBUG = os.environ.get("PYTHONDEVMODE") == "1" - - -@asynccontextmanager -async def debug_query(sql_query: str, query_params: dict | None = None): - """Time the processing time of an sql query.""" - if not ENABLE_DEBUG: - yield - return - time_start = time.time() - try: - yield - except OperationalError as err: - LOGGER.error(f"{err}\n{sql_query}") - raise - finally: - process_time = time.time() - time_start - if process_time > 0.5: - # log slow queries - for key, value in (query_params or {}).items(): - sql_query = sql_query.replace(f":{key}", repr(value)) - LOGGER.warning("SQL Query took %s seconds! (\n%s\n", process_time, sql_query) - - -def query_params(query: str, params: dict[str, Any] | None) -> tuple[str, dict[str, Any]]: - """Extend query parameters support.""" - if params is None: - return (query, params) - count = 0 - result_query = query - result_params = {} - for key, value in params.items(): - # add support for a list within the query params - # recreates the params as (:_param_0, :_param_1) etc - if isinstance(value, list | tuple): - subparams = [] - for subval in value: - subparam_name = f"_param_{count}" - result_params[subparam_name] = subval - subparams.append(subparam_name) - count += 1 - params_str = ",".join(f":{x}" for x in subparams) - result_query = result_query.replace(f" :{key}", f" ({params_str})") - else: - result_params[key] = params[key] - return (result_query, result_params) - - -class DatabaseConnection: - """Class that holds the (connection to the) database with some convenience helper functions.""" - - _db: aiosqlite.Connection - - def __init__(self, db_path: str) -> None: - """Initialize class.""" - self.db_path = db_path - - async def setup(self) -> None: - """Perform async initialization.""" - self._db = await aiosqlite.connect(self.db_path) - self._db.row_factory = aiosqlite.Row - await self.execute("PRAGMA analysis_limit=10000;") - await self.execute("PRAGMA optimize;") - await self.commit() - - async def close(self) -> None: - """Close db connection on exit.""" - await self.execute("PRAGMA optimize;") - await self.commit() - await self._db.close() - - async def get_rows( - self, - table: str, - match: dict | None = None, - order_by: str | None = None, - limit: int = 500, - offset: int = 0, - ) -> list[Mapping]: - """Get all rows for given table.""" - sql_query = f"SELECT * FROM {table}" - if match is not None: - sql_query += " WHERE " + " AND ".join(f"{x} = :{x}" for x in match) - if order_by is not None: - sql_query += f" ORDER BY {order_by}" - if limit: - sql_query += f" LIMIT {limit} OFFSET {offset}" - async with debug_query(sql_query): - return await self._db.execute_fetchall(sql_query, match) - - async def get_rows_from_query( - self, - query: str, - params: dict | None = None, - limit: int = 500, - offset: int = 0, - ) -> list[Mapping]: - """Get all rows for given custom query.""" - if limit: - query += f" LIMIT {limit} OFFSET {offset}" - _query, _params = query_params(query, params) - async with debug_query(_query, _params): - return await self._db.execute_fetchall(_query, _params) - - async def get_count_from_query( - self, - query: str, - params: dict | None = None, - ) -> int: - """Get row count for given custom query.""" - query = f"SELECT count() FROM ({query})" - _query, _params = query_params(query, params) - async with debug_query(_query): - async with self._db.execute(_query, _params) as cursor: - if result := await cursor.fetchone(): - return result[0] - return 0 - - async def get_count( - self, - table: str, - ) -> int: - """Get row count for given table.""" - query = f"SELECT count(*) FROM {table}" - async with debug_query(query): - async with self._db.execute(query) as cursor: - if result := await cursor.fetchone(): - return result[0] - return 0 - - async def search(self, table: str, search: str, column: str = "name") -> list[Mapping]: - """Search table by column.""" - sql_query = f"SELECT * FROM {table} WHERE {table}.{column} LIKE :search" - params = {"search": f"%{search}%"} - async with debug_query(sql_query, params): - return await self._db.execute_fetchall(sql_query, params) - - async def get_row(self, table: str, match: dict[str, Any]) -> Mapping | None: - """Get single row for given table where column matches keys/values.""" - sql_query = f"SELECT * FROM {table} WHERE " - sql_query += " AND ".join(f"{table}.{x} = :{x}" for x in match) - async with debug_query(sql_query, match), self._db.execute(sql_query, match) as cursor: - return await cursor.fetchone() - - async def insert( - self, - table: str, - values: dict[str, Any], - allow_replace: bool = False, - ) -> int: - """Insert data in given table.""" - keys = tuple(values.keys()) - if allow_replace: - sql_query = f'INSERT OR REPLACE INTO {table}({",".join(keys)})' - else: - sql_query = f'INSERT INTO {table}({",".join(keys)})' - sql_query += f' VALUES ({",".join(f":{x}" for x in keys)})' - row_id = await self._db.execute_insert(sql_query, values) - await self._db.commit() - return row_id[0] - - async def insert_or_replace(self, table: str, values: dict[str, Any]) -> Mapping: - """Insert or replace data in given table.""" - return await self.insert(table=table, values=values, allow_replace=True) - - async def update( - self, - table: str, - match: dict[str, Any], - values: dict[str, Any], - ) -> Mapping: - """Update record.""" - keys = tuple(values.keys()) - sql_query = f'UPDATE {table} SET {",".join(f"{x}=:{x}" for x in keys)} WHERE ' - sql_query += " AND ".join(f"{x} = :{x}" for x in match) - await self.execute(sql_query, {**match, **values}) - await self._db.commit() - # return updated item - return await self.get_row(table, match) - - async def delete(self, table: str, match: dict | None = None, query: str | None = None) -> None: - """Delete data in given table.""" - assert not (query and "where" in query.lower()) - sql_query = f"DELETE FROM {table} " - if match: - sql_query += " WHERE " + " AND ".join(f"{x} = :{x}" for x in match) - elif query and "where" not in query.lower(): - sql_query += "WHERE " + query - elif query: - sql_query += query - await self.execute(sql_query, match) - await self._db.commit() - - async def delete_where_query(self, table: str, query: str | None = None) -> None: - """Delete data in given table using given where clausule.""" - sql_query = f"DELETE FROM {table} WHERE {query}" - await self.execute(sql_query) - await self._db.commit() - - async def execute(self, query: str, values: dict | None = None) -> Any: - """Execute command on the database.""" - return await self._db.execute(query, values) - - async def commit(self) -> None: - """Commit the current transaction.""" - return await self._db.commit() - - async def iter_items( - self, - table: str, - match: dict | None = None, - ) -> AsyncGenerator[Mapping, None]: - """Iterate all items within a table.""" - limit: int = 500 - offset: int = 0 - while True: - next_items = await self.get_rows( - table=table, - match=match, - offset=offset, - limit=limit, - ) - for item in next_items: - yield item - if len(next_items) < limit: - break - await asyncio.sleep(0) # yield to eventloop - offset += limit - - async def vacuum(self) -> None: - """Run vacuum command on database.""" - await self._db.execute("VACUUM") - await self._db.commit() diff --git a/music_assistant/server/helpers/didl_lite.py b/music_assistant/server/helpers/didl_lite.py deleted file mode 100644 index 59b02c65..00000000 --- a/music_assistant/server/helpers/didl_lite.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Helper(s) to create DIDL Lite metadata for Sonos/DLNA players.""" - -from __future__ import annotations - -import datetime -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import MediaType -from music_assistant.constants import MASS_LOGO_ONLINE - -if TYPE_CHECKING: - from music_assistant.common.models.player import PlayerMedia - -# ruff: noqa: E501 - - -def create_didl_metadata(media: PlayerMedia) -> str: - """Create DIDL metadata string from url and PlayerMedia.""" - ext = media.uri.split(".")[-1].split("?")[0] - image_url = media.image_url or MASS_LOGO_ONLINE - if media.media_type in (MediaType.FLOW_STREAM, MediaType.RADIO) or not media.duration: - # flow stream, radio or other duration-less stream - title = media.title or media.uri - return ( - '' - f'' - f"{escape_string(title)}" - f"{escape_string(image_url)}" - f"{media.uri}" - "object.item.audioItem.audioBroadcast" - f"audio/{ext}" - f'{escape_string(media.uri)}' - "" - "" - ) - duration_str = str(datetime.timedelta(seconds=media.duration or 0)) + ".000" - return ( - '' - '' - f"{escape_string(media.title or media.uri)}" - f"{escape_string(media.artist or '')}" - f"{escape_string(media.album or '')}" - f"{escape_string(media.artist or '')}" - f"{int(media.duration or 0)}" - f"{media.uri}" - f"{escape_string(image_url)}" - "object.item.audioItem.audioBroadcast" - f"audio/{ext}" - f'{escape_string(media.uri)}' - "" - "" - ) - - -def escape_string(data: str) -> str: - """Create DIDL-safe string.""" - data = data.replace("&", "&") - # data = data.replace("?", "?") - data = data.replace(">", ">") - return data.replace("<", "<") diff --git a/music_assistant/server/helpers/ffmpeg.py b/music_assistant/server/helpers/ffmpeg.py deleted file mode 100644 index 90e17ad8..00000000 --- a/music_assistant/server/helpers/ffmpeg.py +++ /dev/null @@ -1,323 +0,0 @@ -"""FFMpeg related helpers.""" - -from __future__ import annotations - -import asyncio -import logging -from collections import deque -from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.models.errors import AudioError -from music_assistant.common.models.media_items import AudioFormat, ContentType -from music_assistant.constants import VERBOSE_LOG_LEVEL - -from .process import AsyncProcess -from .util import TimedAsyncGenerator, close_async_generator - -LOGGER = logging.getLogger("ffmpeg") -MINIMAL_FFMPEG_VERSION = 6 - - -class FFMpeg(AsyncProcess): - """FFMpeg wrapped as AsyncProcess.""" - - def __init__( - self, - audio_input: AsyncGenerator[bytes, None] | str | int, - input_format: AudioFormat, - output_format: AudioFormat, - filter_params: list[str] | None = None, - extra_args: list[str] | None = None, - extra_input_args: list[str] | None = None, - audio_output: str | int = "-", - collect_log_history: bool = False, - ) -> None: - """Initialize AsyncProcess.""" - ffmpeg_args = get_ffmpeg_args( - input_format=input_format, - output_format=output_format, - filter_params=filter_params or [], - extra_args=extra_args or [], - input_path=audio_input if isinstance(audio_input, str) else "-", - output_path=audio_output if isinstance(audio_output, str) else "-", - extra_input_args=extra_input_args or [], - loglevel="info", - ) - self.audio_input = audio_input - self.input_format = input_format - self.collect_log_history = collect_log_history - self.log_history: deque[str] = deque(maxlen=100) - self._stdin_task: asyncio.Task | None = None - self._logger_task: asyncio.Task | None = None - super().__init__( - ffmpeg_args, - stdin=True if isinstance(audio_input, str | AsyncGenerator) else audio_input, - stdout=True if isinstance(audio_output, str) else audio_output, - stderr=True, - ) - self.logger = LOGGER - - async def start(self) -> None: - """Perform Async init of process.""" - await super().start() - if self.proc: - self.logger = LOGGER.getChild(str(self.proc.pid)) - clean_args = [] - for arg in self._args[1:]: - if arg.startswith("http"): - clean_args.append("") - elif "/" in arg and "." in arg: - clean_args.append("") - else: - clean_args.append(arg) - args_str = " ".join(clean_args) - self.logger.log(VERBOSE_LOG_LEVEL, "started with args: %s", args_str) - self._logger_task = asyncio.create_task(self._log_reader_task()) - if isinstance(self.audio_input, AsyncGenerator): - self._stdin_task = asyncio.create_task(self._feed_stdin()) - - async def close(self, send_signal: bool = True) -> None: - """Close/terminate the process and wait for exit.""" - if self.closed: - return - if self._stdin_task and not self._stdin_task.done(): - self._stdin_task.cancel() - await super().close(send_signal) - - async def _log_reader_task(self) -> None: - """Read ffmpeg log from stderr.""" - decode_errors = 0 - async for line in self.iter_stderr(): - if self.collect_log_history: - self.log_history.append(line) - if "error" in line or "warning" in line: - self.logger.debug(line) - elif "critical" in line: - self.logger.warning(line) - else: - self.logger.log(VERBOSE_LOG_LEVEL, line) - - if "Invalid data found when processing input" in line: - decode_errors += 1 - if decode_errors >= 50: - self.logger.error(line) - await super().close(True) - - # if streamdetails contenttype is unknown, try parse it from the ffmpeg log - if line.startswith("Stream #") and ": Audio: " in line: - if self.input_format.content_type == ContentType.UNKNOWN: - content_type_raw = line.split(": Audio: ")[1].split(" ")[0] - content_type = ContentType.try_parse(content_type_raw) - self.logger.debug( - "Detected (input) content type: %s (%s)", content_type, content_type_raw - ) - self.input_format.content_type = content_type - del line - - async def _feed_stdin(self) -> None: - """Feed stdin with audio chunks from an AsyncGenerator.""" - if TYPE_CHECKING: - self.audio_input: AsyncGenerator[bytes, None] - generator_exhausted = False - audio_received = False - try: - async for chunk in TimedAsyncGenerator(self.audio_input, 300): - audio_received = True - if self.proc and self.proc.returncode is not None: - raise AudioError("Parent process already exited") - await self.write(chunk) - generator_exhausted = True - if not audio_received: - raise AudioError("No audio data received from source") - except Exception as err: - if isinstance(err, asyncio.CancelledError): - return - self.logger.error( - "Stream error: %s", - str(err) or err.__class__.__name__, - exc_info=err if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else None, - ) - finally: - await self.write_eof() - # we need to ensure that we close the async generator - # if we get cancelled otherwise it keeps lingering forever - if not generator_exhausted: - await close_async_generator(self.audio_input) - - -async def get_ffmpeg_stream( - audio_input: AsyncGenerator[bytes, None] | str, - input_format: AudioFormat, - output_format: AudioFormat, - filter_params: list[str] | None = None, - extra_args: list[str] | None = None, - chunk_size: int | None = None, - extra_input_args: list[str] | None = None, -) -> AsyncGenerator[bytes, None]: - """ - Get the ffmpeg audio stream as async generator. - - Takes care of resampling and/or recoding if needed, - according to player preferences. - """ - async with FFMpeg( - audio_input=audio_input, - input_format=input_format, - output_format=output_format, - filter_params=filter_params, - extra_args=extra_args, - extra_input_args=extra_input_args, - ) as ffmpeg_proc: - # read final chunks from stdout - iterator = ffmpeg_proc.iter_chunked(chunk_size) if chunk_size else ffmpeg_proc.iter_any() - async for chunk in iterator: - yield chunk - - -def get_ffmpeg_args( # noqa: PLR0915 - input_format: AudioFormat, - output_format: AudioFormat, - filter_params: list[str], - extra_args: list[str] | None = None, - input_path: str = "-", - output_path: str = "-", - extra_input_args: list[str] | None = None, - loglevel: str = "error", -) -> list[str]: - """Collect all args to send to the ffmpeg process.""" - if extra_args is None: - extra_args = [] - ffmpeg_present, libsoxr_support, version = get_global_cache_value("ffmpeg_support") - if not ffmpeg_present: - msg = ( - "FFmpeg binary is missing from system." - "Please install ffmpeg on your OS to enable playback." - ) - raise AudioError( - msg, - ) - - major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha())) - if major_version < MINIMAL_FFMPEG_VERSION: - msg = ( - f"FFmpeg version {version} is not supported. " - f"Minimal version required is {MINIMAL_FFMPEG_VERSION}." - ) - raise AudioError(msg) - - # generic args - generic_args = [ - "ffmpeg", - "-hide_banner", - "-loglevel", - loglevel, - "-nostats", - "-ignore_unknown", - "-protocol_whitelist", - "file,hls,http,https,tcp,tls,crypto,pipe,data,fd,rtp,udp", - ] - # collect input args - input_args = [] - if extra_input_args: - input_args += extra_input_args - if input_path.startswith("http"): - # append reconnect options for direct stream from http - input_args += [ - # Reconnect automatically when disconnected before EOF is hit. - "-reconnect", - "1", - # Set the maximum delay in seconds after which to give up reconnecting. - "-reconnect_delay_max", - "30", - # If set then even streamed/non seekable streams will be reconnected on errors. - "-reconnect_streamed", - "1", - # Reconnect automatically in case of TCP/TLS errors during connect. - "-reconnect_on_network_error", - "1", - # A comma separated list of HTTP status codes to reconnect on. - # The list can include specific status codes (e.g. 503) or the strings 4xx / 5xx. - "-reconnect_on_http_error", - "5xx,4xx", - ] - if input_format.content_type.is_pcm(): - input_args += [ - "-ac", - str(input_format.channels), - "-channel_layout", - "mono" if input_format.channels == 1 else "stereo", - "-ar", - str(input_format.sample_rate), - "-acodec", - input_format.content_type.name.lower(), - "-f", - input_format.content_type.value, - "-i", - input_path, - ] - else: - # let ffmpeg auto detect the content type from the metadata/headers - input_args += ["-i", input_path] - - # collect output args - output_args = [] - if output_path.upper() == "NULL": - # devnull stream - output_args = ["-f", "null", "-"] - elif output_format.content_type == ContentType.UNKNOWN: - raise RuntimeError("Invalid output format specified") - elif output_format.content_type == ContentType.AAC: - output_args = ["-f", "adts", "-c:a", "aac", "-b:a", "256k", output_path] - elif output_format.content_type == ContentType.MP3: - output_args = ["-f", "mp3", "-b:a", "320k", output_path] - else: - if output_format.content_type.is_pcm(): - output_args += ["-acodec", output_format.content_type.name.lower()] - # use explicit format identifier for all other - output_args += [ - "-f", - output_format.content_type.value, - "-ar", - str(output_format.sample_rate), - "-ac", - str(output_format.channels), - ] - if output_format.output_format_str == "flac": - # use level 0 compression for fastest encoding - output_args += ["-compression_level", "0"] - output_args += [output_path] - - # edge case: source file is not stereo - downmix to stereo - if input_format.channels > 2 and output_format.channels == 2: - filter_params = [ - "pan=stereo|FL=1.0*FL+0.707*FC+0.707*SL+0.707*LFE|FR=1.0*FR+0.707*FC+0.707*SR+0.707*LFE", - *filter_params, - ] - - # determine if we need to do resampling - if ( - input_format.sample_rate != output_format.sample_rate - or input_format.bit_depth > output_format.bit_depth - ): - # prefer resampling with libsoxr due to its high quality - if libsoxr_support: - resample_filter = "aresample=resampler=soxr:precision=30" - else: - resample_filter = "aresample=resampler=swr" - - # sample rate conversion - if input_format.sample_rate != output_format.sample_rate: - resample_filter += f":osr={output_format.sample_rate}" - - # bit depth conversion: apply dithering when going down to 16 bits - if output_format.bit_depth == 16 and input_format.bit_depth > 16: - resample_filter += ":osf=s16:dither_method=triangular_hp" - - filter_params.append(resample_filter) - - if filter_params and "-filter_complex" not in extra_args: - extra_args += ["-af", ",".join(filter_params)] - - return generic_args + input_args + extra_args + output_args diff --git a/music_assistant/server/helpers/images.py b/music_assistant/server/helpers/images.py deleted file mode 100644 index 931c6372..00000000 --- a/music_assistant/server/helpers/images.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Utilities for image manipulation and retrieval.""" - -from __future__ import annotations - -import asyncio -import itertools -import os -import random -from base64 import b64decode -from collections.abc import Iterable -from io import BytesIO -from typing import TYPE_CHECKING - -import aiofiles -from aiohttp.client_exceptions import ClientError -from PIL import Image, UnidentifiedImageError - -from music_assistant.server.helpers.tags import get_embedded_image -from music_assistant.server.models.metadata_provider import MetadataProvider - -if TYPE_CHECKING: - from music_assistant.common.models.media_items import MediaItemImage - from music_assistant.server import MusicAssistant - from music_assistant.server.models.music_provider import MusicProvider - - -async def get_image_data(mass: MusicAssistant, path_or_url: str, provider: str) -> bytes: - """Create thumbnail from image url.""" - # TODO: add local cache here ! - if prov := mass.get_provider(provider): - prov: MusicProvider | MetadataProvider - if resolved_image := await prov.resolve_image(path_or_url): - if isinstance(resolved_image, bytes): - return resolved_image - if isinstance(resolved_image, str): - path_or_url = resolved_image - # handle HTTP location - if path_or_url.startswith("http"): - try: - async with mass.http_session.get(path_or_url, raise_for_status=True) as resp: - return await resp.read() - except ClientError as err: - raise FileNotFoundError from err - # handle base64 embedded images - if path_or_url.startswith("data:image"): - return b64decode(path_or_url.split(",")[-1]) - # handle FILE location (of type image) - if path_or_url.endswith(("jpg", "JPG", "png", "PNG", "jpeg")): - if await asyncio.to_thread(os.path.isfile, path_or_url): - async with aiofiles.open(path_or_url, "rb") as _file: - return await _file.read() - # use ffmpeg for embedded images - if img_data := await get_embedded_image(path_or_url): - return img_data - msg = f"Image not found: {path_or_url}" - raise FileNotFoundError(msg) - - -async def get_image_thumb( - mass: MusicAssistant, - path_or_url: str, - size: int | None, - provider: str, - image_format: str = "PNG", -) -> bytes: - """Get (optimized) PNG thumbnail from image url.""" - img_data = await get_image_data(mass, path_or_url, provider) - if not img_data or not isinstance(img_data, bytes): - raise FileNotFoundError(f"Image not found: {path_or_url}") - - if not size and image_format.encode() in img_data: - return img_data - - def _create_image(): - data = BytesIO() - try: - img = Image.open(BytesIO(img_data)) - except UnidentifiedImageError: - raise FileNotFoundError(f"Invalid image: {path_or_url}") - if size: - img.thumbnail((size, size), Image.Resampling.LANCZOS) - - mode = "RGBA" if image_format == "PNG" else "RGB" - img.convert(mode).save(data, image_format, optimize=True) - return data.getvalue() - - image_format = image_format.upper() - return await asyncio.to_thread(_create_image) - - -async def create_collage( - mass: MusicAssistant, images: Iterable[MediaItemImage], dimensions: tuple[int] = (1500, 1500) -) -> bytes: - """Create a basic collage image from multiple image urls.""" - image_size = 250 - - def _new_collage(): - return Image.new("RGB", (dimensions[0], dimensions[1]), color=(255, 255, 255, 255)) - - collage = await asyncio.to_thread(_new_collage) - - def _add_to_collage(img_data: bytes, coord_x: int, coord_y: int) -> None: - data = BytesIO(img_data) - photo = Image.open(data).convert("RGB") - photo = photo.resize((image_size, image_size)) - collage.paste(photo, (coord_x, coord_y)) - del data - - # prevent duplicates with a set - images = list(set(images)) - random.shuffle(images) - iter_images = itertools.cycle(images) - - for x_co in range(0, dimensions[0], image_size): - for y_co in range(0, dimensions[1], image_size): - for _ in range(5): - img = next(iter_images) - img_data = await get_image_data(mass, img.path, img.provider) - if img_data: - await asyncio.to_thread(_add_to_collage, img_data, x_co, y_co) - del img_data - break - - def _save_collage(): - final_data = BytesIO() - collage.convert("RGB").save(final_data, "JPEG", optimize=True) - return final_data.getvalue() - - return await asyncio.to_thread(_save_collage) - - -async def get_icon_string(icon_path: str) -> str: - """Get svg icon as string.""" - ext = icon_path.rsplit(".")[-1] - assert ext == "svg" - async with aiofiles.open(icon_path) as _file: - xml_data = await _file.read() - return xml_data.replace("\n", "").strip() diff --git a/music_assistant/server/helpers/logging.py b/music_assistant/server/helpers/logging.py deleted file mode 100644 index 1dac5a3a..00000000 --- a/music_assistant/server/helpers/logging.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Logging utilities. - -A lot in this file has been copied from Home Assistant: -https://github.com/home-assistant/core/blob/e5ccd85e7e26c167d0b73669a88bc3a7614dd456/homeassistant/util/logging.py#L78 - -All rights reserved. -""" - -from __future__ import annotations - -import asyncio -import inspect -import logging -import logging.handlers -import queue -import traceback -from collections.abc import Callable, Coroutine -from functools import partial, wraps -from typing import Any, TypeVar, cast, overload - -_T = TypeVar("_T") - - -class LoggingQueueHandler(logging.handlers.QueueHandler): - """Process the log in another thread.""" - - listener: logging.handlers.QueueListener | None = None - - def prepare(self, record: logging.LogRecord) -> logging.LogRecord: - """Prepare a record for queuing. - - This is added as a workaround for https://bugs.python.org/issue46755 - """ - record = super().prepare(record) - record.stack_info = None - return record - - def handle(self, record: logging.LogRecord) -> Any: - """Conditionally emit the specified logging record. - - Depending on which filters have been added to the handler, push the new - records onto the backing Queue. - - The default python logger Handler acquires a lock - in the parent class which we do not need as - SimpleQueue is already thread safe. - - See https://bugs.python.org/issue24645 - """ - return_value = self.filter(record) - if return_value: - self.emit(record) - return return_value - - def close(self) -> None: - """Tidy up any resources used by the handler. - - This adds shutdown of the QueueListener - """ - super().close() - if not self.listener: - return - self.listener.stop() - self.listener = None - - -def activate_log_queue_handler() -> None: - """Migrate the existing log handlers to use the queue. - - This allows us to avoid blocking I/O and formatting messages - in the event loop as log messages are written in another thread. - """ - simple_queue: queue.SimpleQueue[logging.Handler] = queue.SimpleQueue() - queue_handler = LoggingQueueHandler(simple_queue) - logging.root.addHandler(queue_handler) - - migrated_handlers: list[logging.Handler] = [] - for handler in logging.root.handlers[:]: - if handler is queue_handler: - continue - logging.root.removeHandler(handler) - migrated_handlers.append(handler) - - listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers) - queue_handler.listener = listener - - listener.start() - - -def log_exception(format_err: Callable[..., Any], *args: Any) -> None: - """Log an exception with additional context.""" - module = inspect.getmodule(inspect.stack(context=0)[1].frame) - if module is not None: # noqa: SIM108 - module_name = module.__name__ - else: - # If Python is unable to access the sources files, the call stack frame - # will be missing information, so let's guard. - # https://github.com/home-assistant/core/issues/24982 - module_name = __name__ - - # Do not print the wrapper in the traceback - frames = len(inspect.trace()) - 1 - exc_msg = traceback.format_exc(-frames) - friendly_msg = format_err(*args) - logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) - - -@overload -def catch_log_exception( - func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] -) -> Callable[..., Coroutine[Any, Any, None]]: ... - - -@overload -def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: ... - - -def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: - """Decorate a function func to catch and log exceptions. - - If func is a coroutine function, a coroutine function will be returned. - If func is a callback, a callback will be returned. - """ - # Check for partials to properly determine if coroutine function - check_func = func - while isinstance(check_func, partial): - check_func = check_func.func - - wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] - if asyncio.iscoroutinefunction(check_func): - async_func = cast(Callable[..., Coroutine[Any, Any, None]], func) - - @wraps(async_func) - async def async_wrapper(*args: Any) -> None: - """Catch and log exception.""" - try: - await async_func(*args) - except Exception: - log_exception(format_err, *args) - - wrapper_func = async_wrapper - - else: - - @wraps(func) - def wrapper(*args: Any) -> None: - """Catch and log exception.""" - try: - func(*args) - except Exception: - log_exception(format_err, *args) - - wrapper_func = wrapper - return wrapper_func - - -def catch_log_coro_exception( - target: Coroutine[Any, Any, _T], format_err: Callable[..., Any], *args: Any -) -> Coroutine[Any, Any, _T | None]: - """Decorate a coroutine to catch and log exceptions.""" - - async def coro_wrapper(*args: Any) -> _T | None: - """Catch and log exception.""" - try: - return await target - except Exception: - log_exception(format_err, *args) - return None - - return coro_wrapper(*args) - - -def async_create_catching_coro(target: Coroutine[Any, Any, _T]) -> Coroutine[Any, Any, _T | None]: - """Wrap a coroutine to catch and log exceptions. - - The exception will be logged together with a stacktrace of where the - coroutine was wrapped. - - target: target coroutine. - """ - trace = traceback.extract_stack() - return catch_log_coro_exception( - target, - lambda: "Exception in {} called from\n {}".format( - target.__name__, - "".join(traceback.format_list(trace[:-1])), - ), - ) diff --git a/music_assistant/server/helpers/playlists.py b/music_assistant/server/helpers/playlists.py deleted file mode 100644 index 60f69703..00000000 --- a/music_assistant/server/helpers/playlists.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Helpers for parsing (online and offline) playlists.""" - -from __future__ import annotations - -import configparser -import logging -from dataclasses import dataclass -from typing import TYPE_CHECKING -from urllib.parse import urlparse - -from aiohttp import client_exceptions - -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.server.helpers.util import detect_charset - -if TYPE_CHECKING: - from music_assistant.server import MusicAssistant - - -LOGGER = logging.getLogger(__name__) -HLS_CONTENT_TYPES = ( - # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 - "application/vnd.apple.mpegurl", - # Additional informal types used by Mozilla gecko not included as they - # don't reliably indicate HLS streams -) - - -class IsHLSPlaylist(InvalidDataError): - """The playlist from an HLS stream and should not be parsed.""" - - -@dataclass -class PlaylistItem: - """Playlist item.""" - - path: str - length: str | None = None - title: str | None = None - stream_info: dict[str, str] | None = None - key: str | None = None - - @property - def is_url(self) -> bool: - """Validate the URL can be parsed and at least has scheme + netloc.""" - result = urlparse(self.path) - return all([result.scheme, result.netloc]) - - -def parse_m3u(m3u_data: str) -> list[PlaylistItem]: - """Very simple m3u parser. - - Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py - """ - # From Mozilla gecko source: https://github.com/mozilla/gecko-dev/blob/c4c1adbae87bf2d128c39832d72498550ee1b4b8/dom/media/DecoderTraits.cpp#L47-L52 - - m3u_lines = m3u_data.splitlines() - - playlist = [] - - length = None - title = None - stream_info = None - key = None - - for line in m3u_lines: - line = line.strip() # noqa: PLW2901 - if line.startswith("#EXTINF:"): - # Get length and title from #EXTINF line - info = line.split("#EXTINF:")[1].split(",", 1) - if len(info) != 2: - continue - length = info[0].strip()[0] - if length == "-1": - length = None - title = info[1].strip() - elif line.startswith("#EXT-X-STREAM-INF:"): - # HLS stream properties - # https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-10 - stream_info = {} - for part in line.replace("#EXT-X-STREAM-INF:", "").split(","): - if "=" not in part: - continue - kev_value_parts = part.strip().split("=") - stream_info[kev_value_parts[0]] = kev_value_parts[1] - elif line.startswith("#EXT-X-KEY:"): - key = line.split(",URI=")[1].strip('"') - elif line.startswith("#"): - # Ignore other extensions - continue - elif len(line) != 0: - filepath = line - if "%20" in filepath: - # apparently VLC manages to encode spaces in filenames - filepath = filepath.replace("%20", " ") - # replace Windows directory separators - filepath = filepath.replace("\\", "/") - playlist.append( - PlaylistItem( - path=filepath, length=length, title=title, stream_info=stream_info, key=key - ) - ) - # reset the song variables so it doesn't use the same EXTINF more than once - length = None - title = None - stream_info = None - - return playlist - - -def parse_pls(pls_data: str) -> list[PlaylistItem]: - """Parse (only) filenames/urls from pls playlist file.""" - pls_parser = configparser.ConfigParser() - try: - pls_parser.read_string(pls_data, "playlist") - except configparser.Error as err: - raise InvalidDataError("Can't parse playlist") from err - - if "playlist" not in pls_parser or pls_parser["playlist"].getint("Version") != 2: - raise InvalidDataError("Invalid playlist") - - try: - num_entries = pls_parser.getint("playlist", "NumberOfEntries") - except (configparser.NoOptionError, ValueError) as err: - raise InvalidDataError("Invalid NumberOfEntries in playlist") from err - - playlist_section = pls_parser["playlist"] - - playlist = [] - for entry in range(1, num_entries + 1): - file_option = f"File{entry}" - if file_option not in playlist_section: - continue - itempath = playlist_section[file_option] - length = playlist_section.get(f"Length{entry}") - playlist.append( - PlaylistItem( - length=length if length and length != "-1" else None, - title=playlist_section.get(f"Title{entry}"), - path=itempath, - ) - ) - return playlist - - -async def fetch_playlist( - mass: MusicAssistant, url: str, raise_on_hls: bool = True -) -> list[PlaylistItem]: - """Parse an online m3u or pls playlist.""" - try: - async with mass.http_session.get(url, allow_redirects=True, timeout=5) as resp: - try: - raw_data = await resp.content.read(64 * 1024) - # NOTE: using resp.charset is not reliable, we need to detect it ourselves - encoding = resp.charset or await detect_charset(raw_data) - playlist_data = raw_data.decode(encoding, errors="replace") - except (ValueError, UnicodeDecodeError) as err: - msg = f"Could not decode playlist {url}" - raise InvalidDataError(msg) from err - except TimeoutError as err: - msg = f"Timeout while fetching playlist {url}" - raise InvalidDataError(msg) from err - except client_exceptions.ClientError as err: - msg = f"Error while fetching playlist {url}" - raise InvalidDataError(msg) from err - - if raise_on_hls and "#EXT-X-VERSION:" in playlist_data or "#EXT-X-STREAM-INF:" in playlist_data: - raise IsHLSPlaylist - - if url.endswith((".m3u", ".m3u8")): - playlist = parse_m3u(playlist_data) - else: - playlist = parse_pls(playlist_data) - - if not playlist: - msg = f"Empty playlist {url}" - raise InvalidDataError(msg) - - return playlist diff --git a/music_assistant/server/helpers/process.py b/music_assistant/server/helpers/process.py deleted file mode 100644 index 21d0ef03..00000000 --- a/music_assistant/server/helpers/process.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -AsyncProcess. - -Wrapper around asyncio subprocess to help with using pipe streams and -taking care of properly closing the process in case of exit (on both success and failures), -without deadlocking. -""" - -from __future__ import annotations - -import asyncio -import logging -import os - -# if TYPE_CHECKING: -from collections.abc import AsyncGenerator -from contextlib import suppress -from signal import SIGINT -from types import TracebackType -from typing import Self - -from music_assistant.constants import MASS_LOGGER_NAME, VERBOSE_LOG_LEVEL - -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.helpers.process") - -DEFAULT_CHUNKSIZE = 64000 - - -class AsyncProcess: - """ - AsyncProcess. - - Wrapper around asyncio subprocess to help with using pipe streams and - taking care of properly closing the process in case of exit (on both success and failures), - without deadlocking. - """ - - def __init__( - self, - args: list[str], - stdin: bool | int | None = None, - stdout: bool | int | None = None, - stderr: bool | int | None = False, - name: str | None = None, - ) -> None: - """Initialize AsyncProcess.""" - self.proc: asyncio.subprocess.Process | None = None - if name is None: - name = args[0].split(os.sep)[-1] - self.name = name - self.logger = LOGGER.getChild(name) - self._args = args - self._stdin = None if stdin is False else stdin - self._stdout = None if stdout is False else stdout - self._stderr = asyncio.subprocess.DEVNULL if stderr is False else stderr - self._close_called = False - self._returncode: bool | None = None - - @property - def closed(self) -> bool: - """Return if the process was closed.""" - return self._close_called or self.returncode is not None - - @property - def returncode(self) -> int | None: - """Return the erturncode of the process.""" - if self._returncode is not None: - return self._returncode - if self.proc is None: - return None - if (ret_code := self.proc.returncode) is not None: - self._returncode = ret_code - return ret_code - - async def __aenter__(self) -> Self: - """Enter context manager.""" - await self.start() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - # send interrupt signal to process when we're cancelled - await self.close(send_signal=exc_type in (GeneratorExit, asyncio.CancelledError)) - self._returncode = self.returncode - - async def start(self) -> None: - """Perform Async init of process.""" - for attempt in range(2): - try: - self.proc = await asyncio.create_subprocess_exec( - *self._args, - stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, - stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, - stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, - # because we're exchanging big amounts of (audio) data with pipes - # it makes sense to extend the pipe size and (buffer) limits a bit - limit=1000000 if attempt == 0 else 65536, - pipesize=1000000 if attempt == 0 else -1, - ) - break - except PermissionError: - if attempt > 0: - raise - LOGGER.error( - "Detected that you are running the (docker) container without " - "permissive access rights. This will impact performance !" - ) - - self.logger.log( - VERBOSE_LOG_LEVEL, "Process %s started with PID %s", self.name, self.proc.pid - ) - - async def iter_chunked(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]: - """Yield chunks of n size from the process stdout.""" - while True: - chunk = await self.readexactly(n) - if len(chunk) == 0: - break - yield chunk - - async def iter_any(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]: - """Yield chunks as they come in from process stdout.""" - while True: - chunk = await self.read(n) - if len(chunk) == 0: - break - yield chunk - - async def readexactly(self, n: int) -> bytes: - """Read exactly n bytes from the process stdout (or less if eof).""" - if self._close_called: - return b"" - try: - return await self.proc.stdout.readexactly(n) - except asyncio.IncompleteReadError as err: - return err.partial - - async def read(self, n: int) -> bytes: - """Read up to n bytes from the stdout stream. - - If n is positive, this function try to read n bytes, - and may return less or equal bytes than requested, but at least one byte. - If EOF was received before any byte is read, this function returns empty byte object. - """ - if self._close_called: - return b"" - return await self.proc.stdout.read(n) - - async def write(self, data: bytes) -> None: - """Write data to process stdin.""" - if self.closed: - self.logger.warning("write called while process already done") - return - self.proc.stdin.write(data) - with suppress(BrokenPipeError, ConnectionResetError): - await self.proc.stdin.drain() - - async def write_eof(self) -> None: - """Write end of file to to process stdin.""" - if self.closed: - return - try: - if self.proc.stdin.can_write_eof(): - self.proc.stdin.write_eof() - except ( - AttributeError, - AssertionError, - BrokenPipeError, - RuntimeError, - ConnectionResetError, - ): - # already exited, race condition - pass - - async def read_stderr(self) -> bytes: - """Read line from stderr.""" - if self._close_called: - return b"" - try: - return await self.proc.stderr.readline() - except ValueError as err: - # we're waiting for a line (separator found), but the line was too big - # this may happen with ffmpeg during a long (radio) stream where progress - # gets outputted to the stderr but no newline - # https://stackoverflow.com/questions/55457370/how-to-avoid-valueerror-separator-is-not-found-and-chunk-exceed-the-limit - # NOTE: this consumes the line that was too big - if "chunk exceed the limit" in str(err): - return await self.proc.stderr.readline() - # raise for all other (value) errors - raise - - async def iter_stderr(self) -> AsyncGenerator[str, None]: - """Iterate lines from the stderr stream as string.""" - while True: - line = await self.read_stderr() - if line == b"": - break - line = line.decode("utf-8", errors="ignore").strip() - if not line: - continue - yield line - - async def close(self, send_signal: bool = False) -> None: - """Close/terminate the process and wait for exit.""" - self._close_called = True - if send_signal and self.returncode is None: - self.proc.send_signal(SIGINT) - if self.proc.stdin and not self.proc.stdin.is_closing(): - self.proc.stdin.close() - # abort existing readers on stderr/stdout first before we send communicate - waiter: asyncio.Future - if self.proc.stdout and (waiter := self.proc.stdout._waiter): - self.proc.stdout._waiter = None - if waiter and not waiter.done(): - waiter.set_exception(asyncio.CancelledError()) - if self.proc.stderr and (waiter := self.proc.stderr._waiter): - self.proc.stderr._waiter = None - if waiter and not waiter.done(): - waiter.set_exception(asyncio.CancelledError()) - await asyncio.sleep(0) # yield to loop - - # make sure the process is really cleaned up. - # especially with pipes this can cause deadlocks if not properly guarded - # we need to ensure stdout and stderr are flushed and stdin closed - while self.returncode is None: - try: - # use communicate to flush all pipe buffers - await asyncio.wait_for(self.proc.communicate(), 5) - except RuntimeError as err: - if "read() called while another coroutine" in str(err): - # race condition - continue - raise - except TimeoutError: - self.logger.debug( - "Process %s with PID %s did not stop in time. Sending terminate...", - self.name, - self.proc.pid, - ) - self.proc.terminate() - self.logger.log( - VERBOSE_LOG_LEVEL, - "Process %s with PID %s stopped with returncode %s", - self.name, - self.proc.pid, - self.returncode, - ) - - async def wait(self) -> int: - """Wait for the process and return the returncode.""" - if self._returncode is None: - self._returncode = await self.proc.wait() - return self._returncode - - -async def check_output(*args: str, env: dict[str, str] | None = None) -> tuple[int, bytes]: - """Run subprocess and return returncode and output.""" - proc = await asyncio.create_subprocess_exec( - *args, stderr=asyncio.subprocess.STDOUT, stdout=asyncio.subprocess.PIPE, env=env - ) - stdout, _ = await proc.communicate() - return (proc.returncode, stdout) - - -async def communicate( - args: list[str], - input: bytes | None = None, # noqa: A002 -) -> tuple[int, bytes, bytes]: - """Communicate with subprocess and return returncode, stdout and stderr output.""" - proc = await asyncio.create_subprocess_exec( - *args, - stderr=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE if input is not None else None, - ) - stdout, stderr = await proc.communicate(input) - return (proc.returncode, stdout, stderr) diff --git a/music_assistant/server/helpers/resources/announce.mp3 b/music_assistant/server/helpers/resources/announce.mp3 deleted file mode 100644 index 6e2fa0ab..00000000 Binary files a/music_assistant/server/helpers/resources/announce.mp3 and /dev/null differ diff --git a/music_assistant/server/helpers/resources/fallback_fanart.jpeg b/music_assistant/server/helpers/resources/fallback_fanart.jpeg deleted file mode 100644 index 24528fbe..00000000 Binary files a/music_assistant/server/helpers/resources/fallback_fanart.jpeg and /dev/null differ diff --git a/music_assistant/server/helpers/resources/logo.png b/music_assistant/server/helpers/resources/logo.png deleted file mode 100644 index d00d8ffd..00000000 Binary files a/music_assistant/server/helpers/resources/logo.png and /dev/null differ diff --git a/music_assistant/server/helpers/resources/silence.mp3 b/music_assistant/server/helpers/resources/silence.mp3 deleted file mode 100644 index 38febc1b..00000000 Binary files a/music_assistant/server/helpers/resources/silence.mp3 and /dev/null differ diff --git a/music_assistant/server/helpers/tags.py b/music_assistant/server/helpers/tags.py deleted file mode 100644 index ba40710a..00000000 --- a/music_assistant/server/helpers/tags.py +++ /dev/null @@ -1,481 +0,0 @@ -"""Helpers/utilities to parse ID3 tags from audio files with ffmpeg.""" - -from __future__ import annotations - -import asyncio -import json -import logging -import os -from collections.abc import Iterable -from dataclasses import dataclass -from json import JSONDecodeError -from typing import Any - -import eyed3 - -from music_assistant.common.helpers.util import try_parse_int -from music_assistant.common.models.enums import AlbumType -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.common.models.media_items import MediaItemChapter -from music_assistant.constants import MASS_LOGGER_NAME, UNKNOWN_ARTIST -from music_assistant.server.helpers.process import AsyncProcess - -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.tags") - -# silence the eyed3 logger because it is too verbose -logging.getLogger("eyed3").setLevel(logging.ERROR) - - -# the only multi-item splitter we accept is the semicolon, -# which is also the default in Musicbrainz Picard. -# the slash is also a common splitter but causes collisions with -# artists actually containing a slash in the name, such as AC/DC -TAG_SPLITTER = ";" - - -def clean_tuple(values: Iterable[str]) -> tuple: - """Return a tuple with all empty values removed.""" - return tuple(x.strip() for x in values if x not in (None, "", " ")) - - -def split_items(org_str: str, allow_unsafe_splitters: bool = False) -> tuple[str, ...]: - """Split up a tags string by common splitter.""" - if org_str is None: - return () - if isinstance(org_str, list): - return (x.strip() for x in org_str) - org_str = org_str.strip() - if TAG_SPLITTER in org_str: - return clean_tuple(org_str.split(TAG_SPLITTER)) - if allow_unsafe_splitters and "/" in org_str: - return clean_tuple(org_str.split("/")) - if allow_unsafe_splitters and ", " in org_str: - return clean_tuple(org_str.split(", ")) - return clean_tuple((org_str,)) - - -def split_artists( - org_artists: str | tuple[str, ...], allow_ampersand: bool = False -) -> tuple[str, ...]: - """Parse all artists from a string.""" - final_artists = set() - # when not using the multi artist tag, the artist string may contain - # multiple artists in freeform, even featuring artists may be included in this - # string. Try to parse the featuring artists and separate them. - splitters = ("featuring", " feat. ", " feat ", "feat.") - if allow_ampersand: - splitters = (*splitters, " & ") - artists = split_items(org_artists) - for item in artists: - for splitter in splitters: - if splitter not in item: - continue - for subitem in item.split(splitter): - final_artists.add(subitem.strip()) - if not final_artists: - # none of the extra splitters was found - return artists - return tuple(final_artists) - - -@dataclass -class AudioTags: - """Audio metadata parsed from an audio file.""" - - raw: dict[str, Any] - sample_rate: int - channels: int - bits_per_sample: int - format: str - bit_rate: int | None - duration: float | None - tags: dict[str, str] - has_cover_image: bool - filename: str - - @property - def title(self) -> str: - """Return title tag (as-is).""" - if tag := self.tags.get("title"): - return tag - # fallback to parsing from filename - title = self.filename.rsplit(os.sep, 1)[-1].split(".")[0] - if " - " in title: - title_parts = title.split(" - ") - if len(title_parts) >= 2: - return title_parts[1].strip() - return title - - @property - def version(self) -> str: - """Return version tag (as-is).""" - if tag := self.tags.get("version"): - return tag - album_type_tag = ( - self.tags.get("musicbrainzalbumtype") - or self.tags.get("albumtype") - or self.tags.get("releasetype") - ) - if album_type_tag and "live" in album_type_tag.lower(): - # yes, this can happen - return "Live" - return "" - - @property - def album(self) -> str | None: - """Return album tag (as-is) if present.""" - return self.tags.get("album") - - @property - def artists(self) -> tuple[str, ...]: - """Return track artists.""" - # prefer multi-artist tag - if tag := self.tags.get("artists"): - return split_items(tag) - # fallback to regular artist string - if tag := self.tags.get("artist"): - if TAG_SPLITTER in tag: - return split_items(tag) - return split_artists(tag) - # fallback to parsing from filename - title = self.filename.rsplit(os.sep, 1)[-1].split(".")[0] - if " - " in title: - title_parts = title.split(" - ") - if len(title_parts) >= 2: - return split_artists(title_parts[0]) - return (UNKNOWN_ARTIST,) - - @property - def album_artists(self) -> tuple[str, ...]: - """Return (all) album artists (if any).""" - # prefer multi-artist tag - if tag := self.tags.get("albumartists"): - return split_items(tag) - # fallback to regular artist string - if tag := self.tags.get("albumartist"): - if TAG_SPLITTER in tag: - return split_items(tag) - if len(self.musicbrainz_albumartistids) > 1: - # special case: album artist noted as 2 artists with ampersand - # but with 2 mb ids so they should be treated as 2 artists - # example: John Travolta & Olivia Newton John on the Grease album - return split_artists(tag, allow_ampersand=True) - return split_artists(tag) - return () - - @property - def genres(self) -> tuple[str, ...]: - """Return (all) genres, if any.""" - return split_items(self.tags.get("genre")) - - @property - def disc(self) -> int | None: - """Return disc tag if present.""" - if tag := self.tags.get("disc"): - return try_parse_int(tag.split("/")[0], None) - return None - - @property - def track(self) -> int | None: - """Return track tag if present.""" - if tag := self.tags.get("track"): - return try_parse_int(tag.split("/")[0], None) - # fallback to parsing from filename (if present) - # this can be in the form of 01 - title.mp3 - # or 01-title.mp3 - # or 01.title.mp3 - # or 01 title.mp3 - # or 1. title.mp3 - for splitpos in (4, 3, 2, 1): - firstpart = self.filename[:splitpos] - if firstpart.isnumeric(): - return try_parse_int(firstpart, None) - return None - - @property - def year(self) -> int | None: - """Return album's year if present, parsed from date.""" - if tag := self.tags.get("originalyear"): - return try_parse_int(tag.split("-")[0], None) - if tag := self.tags.get("originaldate"): - return try_parse_int(tag.split("-")[0], None) - if tag := self.tags.get("date"): - return try_parse_int(tag.split("-")[0], None) - return None - - @property - def musicbrainz_artistids(self) -> tuple[str, ...]: - """Return musicbrainz_artistid tag(s) if present.""" - return split_items(self.tags.get("musicbrainzartistid"), True) - - @property - def musicbrainz_albumartistids(self) -> tuple[str, ...]: - """Return musicbrainz_albumartistid tag if present.""" - if tag := self.tags.get("musicbrainzalbumartistid"): - return split_items(tag, True) - return split_items(self.tags.get("musicbrainzreleaseartistid"), True) - - @property - def musicbrainz_releasegroupid(self) -> str | None: - """Return musicbrainz_releasegroupid tag if present.""" - return self.tags.get("musicbrainzreleasegroupid") - - @property - def musicbrainz_albumid(self) -> str | None: - """Return musicbrainz_albumid tag if present.""" - return self.tags.get("musicbrainzreleaseid", self.tags.get("musicbrainzalbumid")) - - @property - def musicbrainz_recordingid(self) -> str | None: - """Return musicbrainz_recordingid tag if present.""" - if tag := self.tags.get("UFID:http://musicbrainz.org"): - return tag - if tag := self.tags.get("musicbrainz.org"): - return tag - if tag := self.tags.get("musicbrainzrecordingid"): - return tag - return self.tags.get("musicbrainztrackid") - - @property - def title_sort(self) -> str | None: - """Return sort title tag (if exists).""" - if tag := self.tags.get("titlesort"): - return tag - return None - - @property - def album_sort(self) -> str | None: - """Return album sort title tag (if exists).""" - if tag := self.tags.get("albumsort"): - return tag - return None - - @property - def artist_sort_names(self) -> tuple[str, ...]: - """Return artist sort name tag(s) if present.""" - return split_items(self.tags.get("artistsort"), False) - - @property - def album_artist_sort_names(self) -> tuple[str, ...]: - """Return artist sort name tag(s) if present.""" - return split_items(self.tags.get("albumartistsort"), False) - - @property - def album_type(self) -> AlbumType: - """Return albumtype tag if present.""" - # handle audiobook/podcast - if self.filename.endswith("m4b") and len(self.chapters) > 1: - return AlbumType.AUDIOBOOK - if "podcast" in self.tags.get("genre", "").lower() and len(self.chapters) > 1: - return AlbumType.PODCAST - if self.tags.get("compilation", "") == "1": - return AlbumType.COMPILATION - tag = ( - self.tags.get("musicbrainzalbumtype") - or self.tags.get("albumtype") - or self.tags.get("releasetype") - ) - if tag is None: - return AlbumType.UNKNOWN - # the album type tag is messy within id3 and may even contain multiple types - # try to parse one in order of preference - for album_type in ( - AlbumType.PODCAST, - AlbumType.AUDIOBOOK, - AlbumType.COMPILATION, - AlbumType.EP, - AlbumType.SINGLE, - AlbumType.ALBUM, - ): - if album_type.value in tag.lower(): - return album_type - - return AlbumType.UNKNOWN - - @property - def isrc(self) -> tuple[str]: - """Return isrc tag(s).""" - for tag_name in ("isrc", "tsrc"): - if tag := self.tags.get(tag_name): - # sometimes the field contains multiple values - return split_items(tag, True) - return () - - @property - def barcode(self) -> str | None: - """Return barcode (upc/ean) tag(s).""" - for tag_name in ("barcode", "upc", "ean"): - if tag := self.tags.get(tag_name): - # sometimes the field contains multiple values - # we only need one - for item in split_items(tag, True): - if len(item) == 12: - # convert UPC barcode to EAN-13 - return f"0{item}" - return item - return None - - @property - def chapters(self) -> list[MediaItemChapter]: - """Return chapters in MediaItem (if any).""" - chapters: list[MediaItemChapter] = [] - if raw_chapters := self.raw.get("chapters"): - for chapter_data in raw_chapters: - chapters.append( - MediaItemChapter( - chapter_id=chapter_data["id"], - position_start=chapter_data["start"], - position_end=chapter_data["end"], - title=chapter_data.get("tags", {}).get("title"), - ) - ) - return chapters - - @property - def lyrics(self) -> str | None: - """Return lyrics tag (if exists).""" - for key, value in self.tags.items(): - if key.startswith("lyrics"): - return value - return None - - @property - def track_loudness(self) -> float | None: - """Try to read/calculate the integrated loudness from the tags.""" - if (tag := self.tags.get("r128trackgain")) is not None: - return -23 - float(int(tag.split(" ")[0]) / 256) - if (tag := self.tags.get("replaygaintrackgain")) is not None: - return -18 - float(tag.split(" ")[0]) - return None - - @property - def track_album_loudness(self) -> float | None: - """Try to read/calculate the integrated loudness from the tags (album level).""" - if tag := self.tags.get("r128albumgain"): - return -23 - float(int(tag.split(" ")[0]) / 256) - if (tag := self.tags.get("replaygainalbumgain")) is not None: - return -18 - float(tag.split(" ")[0]) - return None - - @classmethod - def parse(cls, raw: dict) -> AudioTags: - """Parse instance from raw ffmpeg info output.""" - audio_stream = next((x for x in raw["streams"] if x["codec_type"] == "audio"), None) - if audio_stream is None: - msg = "No audio stream found" - raise InvalidDataError(msg) - has_cover_image = any( - x for x in raw["streams"] if x.get("codec_name", "") in ("mjpeg", "png") - ) - # convert all tag-keys (gathered from all streams) to lowercase without spaces - tags = {} - for stream in raw["streams"] + [raw["format"]]: - for key, value in stream.get("tags", {}).items(): - alt_key = key.lower().replace(" ", "").replace("_", "").replace("-", "") - tags[alt_key] = value - - return AudioTags( - raw=raw, - sample_rate=int(audio_stream.get("sample_rate", 44100)), - channels=audio_stream.get("channels", 2), - bits_per_sample=int( - audio_stream.get("bits_per_raw_sample", audio_stream.get("bits_per_sample")) or 16 - ), - format=raw["format"]["format_name"], - bit_rate=int(raw["format"].get("bit_rate", 0)) or None, - duration=float(raw["format"].get("duration", 0)) or None, - tags=tags, - has_cover_image=has_cover_image, - filename=raw["format"]["filename"], - ) - - def get(self, key: str, default=None) -> Any: - """Get tag by key.""" - return self.tags.get(key, default) - - -async def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags: - """ - Parse tags from a media file (or URL). - - Input_file may be a (local) filename or URL accessible by ffmpeg. - """ - args = ( - "ffprobe", - "-hide_banner", - "-loglevel", - "fatal", - "-threads", - "0", - "-show_error", - "-show_format", - "-show_streams", - "-show_chapters", - "-print_format", - "json", - "-i", - input_file, - ) - async with AsyncProcess(args, stdin=False, stdout=True) as ffmpeg: - res = await ffmpeg.read(-1) - try: - data = json.loads(res) - if error := data.get("error"): - raise InvalidDataError(error["string"]) - if not data.get("streams"): - msg = "Not an audio file" - raise InvalidDataError(msg) - tags = AudioTags.parse(data) - del res - del data - if not tags.duration and file_size and tags.bit_rate: - # estimate duration from filesize/bitrate - tags.duration = int((file_size * 8) / tags.bit_rate) - if not tags.duration and tags.raw.get("format", {}).get("duration"): - tags.duration = float(tags.raw["format"]["duration"]) - - if ( - not input_file.startswith("http") - and input_file.endswith(".mp3") - and "musicbrainzrecordingid" not in tags.tags - and await asyncio.to_thread(os.path.isfile, input_file) - ): - # eyed3 is able to extract the musicbrainzrecordingid from the unique file id - # this is actually a bug in ffmpeg/ffprobe which does not expose this tag - # so we use this as alternative approach for mp3 files - audiofile = await asyncio.to_thread(eyed3.load, input_file) - if audiofile is not None and audiofile.tag is not None: - for uf_id in audiofile.tag.unique_file_ids: - if uf_id.owner_id == b"http://musicbrainz.org" and uf_id.uniq_id: - tags.tags["musicbrainzrecordingid"] = uf_id.uniq_id.decode() - break - del audiofile - return tags - except (KeyError, ValueError, JSONDecodeError, InvalidDataError) as err: - msg = f"Unable to retrieve info for {input_file}: {err!s}" - raise InvalidDataError(msg) from err - - -async def get_embedded_image(input_file: str) -> bytes | None: - """Return embedded image data. - - Input_file may be a (local) filename or URL accessible by ffmpeg. - """ - args = ( - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-i", - input_file, - "-an", - "-vcodec", - "mjpeg", - "-f", - "mjpeg", - "-", - ) - async with AsyncProcess( - args, stdin=False, stdout=True, stderr=None, name="ffmpeg_image" - ) as ffmpeg: - return await ffmpeg.read(-1) diff --git a/music_assistant/server/helpers/throttle_retry.py b/music_assistant/server/helpers/throttle_retry.py deleted file mode 100644 index 59bd9fb0..00000000 --- a/music_assistant/server/helpers/throttle_retry.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Context manager using asyncio_throttle that catches and re-raises RetriesExhausted.""" - -import asyncio -import functools -import logging -import time -from collections import deque -from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine -from contextlib import asynccontextmanager -from contextvars import ContextVar -from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar - -from music_assistant.common.models.errors import ResourceTemporarilyUnavailable, RetriesExhausted -from music_assistant.constants import MASS_LOGGER_NAME - -if TYPE_CHECKING: - from music_assistant.server.models.provider import Provider - -_ProviderT = TypeVar("_ProviderT", bound="Provider") -_R = TypeVar("_R") -_P = ParamSpec("_P") -LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.throttle_retry") - -BYPASS_THROTTLER: ContextVar[bool] = ContextVar("BYPASS_THROTTLER", default=False) - - -class Throttler: - """asyncio_throttle (https://github.com/hallazzang/asyncio-throttle). - - With improvements: - - Accurate sleep without "busy waiting" (PR #4) - - Return the delay caused by acquire() - """ - - def __init__(self, rate_limit: int, period=1.0): - """Initialize the Throttler.""" - self.rate_limit = rate_limit - self.period = period - self._task_logs: deque[float] = deque() - - def _flush(self): - now = time.monotonic() - while self._task_logs: - if now - self._task_logs[0] > self.period: - self._task_logs.popleft() - else: - break - - async def acquire(self) -> float: - """Acquire a free slot from the Throttler, returns the throttled time.""" - cur_time = time.monotonic() - start_time = cur_time - while True: - self._flush() - if len(self._task_logs) < self.rate_limit: - break - # sleep the exact amount of time until the oldest task can be flushed - time_to_release = self._task_logs[0] + self.period - cur_time - await asyncio.sleep(time_to_release) - cur_time = time.monotonic() - - self._task_logs.append(cur_time) - return cur_time - start_time # exactly 0 if not throttled - - async def __aenter__(self) -> float: - """Wait until the lock is acquired, return the time delay.""" - return await self.acquire() - - async def __aexit__(self, exc_type, exc, tb): - """Nothing to do on exit.""" - - -class ThrottlerManager: - """Throttler manager that extends asyncio Throttle by retrying.""" - - def __init__(self, rate_limit: int, period: float = 1, retry_attempts=5, initial_backoff=5): - """Initialize the AsyncThrottledContextManager.""" - self.retry_attempts = retry_attempts - self.initial_backoff = initial_backoff - self.throttler = Throttler(rate_limit, period) - - @asynccontextmanager - async def acquire(self) -> AsyncGenerator[None, float]: - """Acquire a free slot from the Throttler, returns the throttled time.""" - if BYPASS_THROTTLER.get(): - yield 0 - else: - yield await self.throttler.acquire() - - @asynccontextmanager - async def bypass(self) -> AsyncGenerator[None, None]: - """Bypass the throttler.""" - try: - token = BYPASS_THROTTLER.set(True) - yield None - finally: - BYPASS_THROTTLER.reset(token) - - -def throttle_with_retries( - func: Callable[Concatenate[_ProviderT, _P], Awaitable[_R]], -) -> Callable[Concatenate[_ProviderT, _P], Coroutine[Any, Any, _R]]: - """Call async function using the throttler with retries.""" - - @functools.wraps(func) - async def wrapper(self: _ProviderT, *args: _P.args, **kwargs: _P.kwargs) -> _R: - """Call async function using the throttler with retries.""" - # the trottler attribute must be present on the class - throttler: ThrottlerManager = self.throttler - backoff_time = throttler.initial_backoff - async with throttler.acquire() as delay: - if delay != 0: - self.logger.debug( - "%s was delayed for %.3f secs due to throttling", func.__name__, delay - ) - for attempt in range(throttler.retry_attempts): - try: - return await func(self, *args, **kwargs) - except ResourceTemporarilyUnavailable as e: - backoff_time = e.backoff_time or backoff_time - self.logger.info( - f"Attempt {attempt + 1}/{throttler.retry_attempts} failed: {e}" - ) - if attempt < throttler.retry_attempts - 1: - self.logger.info(f"Retrying in {backoff_time} seconds...") - await asyncio.sleep(backoff_time) - backoff_time *= 2 - else: # noqa: PLW0120 - msg = f"Retries exhausted, failed after {throttler.retry_attempts} attempts" - raise RetriesExhausted(msg) - - return wrapper diff --git a/music_assistant/server/helpers/util.py b/music_assistant/server/helpers/util.py deleted file mode 100644 index 3d342b73..00000000 --- a/music_assistant/server/helpers/util.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Various (server-only) tools and helpers.""" - -from __future__ import annotations - -import asyncio -import functools -import importlib -import logging -import platform -import tempfile -import urllib.error -import urllib.parse -import urllib.request -from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine -from contextlib import suppress -from functools import lru_cache -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as pkg_version -from types import TracebackType -from typing import TYPE_CHECKING, Any, ParamSpec, Self, TypeVar - -import cchardet as chardet -import ifaddr -import memory_tempfile -from zeroconf import IPVersion - -from music_assistant.server.helpers.process import check_output - -if TYPE_CHECKING: - from collections.abc import Iterator - - from zeroconf.asyncio import AsyncServiceInfo - - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderModuleType - -LOGGER = logging.getLogger(__name__) - -HA_WHEELS = "https://wheels.home-assistant.io/musllinux/" - - -async def install_package(package: str) -> None: - """Install package with pip, raise when install failed.""" - LOGGER.debug("Installing python package %s", package) - args = ["uv", "pip", "install", "--no-cache", "--find-links", HA_WHEELS, package] - return_code, output = await check_output(*args) - - if return_code != 0 and "Permission denied" in output.decode(): - # try again with regular pip - # uv pip seems to have issues with permissions on docker installs - args = [ - "pip", - "install", - "--no-cache-dir", - "--no-input", - "--find-links", - HA_WHEELS, - package, - ] - return_code, output = await check_output(*args) - - if return_code != 0: - msg = f"Failed to install package {package}\n{output.decode()}" - raise RuntimeError(msg) - - -async def get_package_version(pkg_name: str) -> str | None: - """ - Return the version of an installed (python) package. - - Will return None if the package is not found. - """ - try: - return await asyncio.to_thread(pkg_version, pkg_name) - except PackageNotFoundError: - return None - - -async def get_ips(include_ipv6: bool = False, ignore_loopback: bool = True) -> set[str]: - """Return all IP-adresses of all network interfaces.""" - - def call() -> set[str]: - result: set[str] = set() - adapters = ifaddr.get_adapters() - for adapter in adapters: - for ip in adapter.ips: - if ip.is_IPv6 and not include_ipv6: - continue - if ip.ip == "127.0.0.1" and ignore_loopback: - continue - result.add(ip.ip) - return result - - return await asyncio.to_thread(call) - - -async def is_hass_supervisor() -> bool: - """Return if we're running inside the HA Supervisor (e.g. HAOS).""" - - def _check(): - try: - urllib.request.urlopen("http://supervisor/core", timeout=1) - except urllib.error.URLError as err: - # this should return a 401 unauthorized if it exists - return getattr(err, "code", 999) == 401 - except Exception: - return False - return False - - return await asyncio.to_thread(_check) - - -async def load_provider_module(domain: str, requirements: list[str]) -> ProviderModuleType: - """Return module for given provider domain and make sure the requirements are met.""" - - @lru_cache - def _get_provider_module(domain: str) -> ProviderModuleType: - return importlib.import_module(f".{domain}", "music_assistant.server.providers") - - # ensure module requirements are met - for requirement in requirements: - if "==" not in requirement: - # we should really get rid of unpinned requirements - continue - package_name, version = requirement.split("==", 1) - installed_version = await get_package_version(package_name) - if installed_version == "0.0.0": - # ignore editable installs - continue - if installed_version != version: - await install_package(requirement) - - # try to load the module - try: - return await asyncio.to_thread(_get_provider_module, domain) - except ImportError: - # (re)install ALL requirements - for requirement in requirements: - await install_package(requirement) - # try loading the provider again to be safe - # this will fail if something else is wrong (as it should) - return await asyncio.to_thread(_get_provider_module, domain) - - -def create_tempfile(): - """Return a (named) temporary file.""" - # ruff: noqa: SIM115 - if platform.system() == "Linux": - return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) - return tempfile.NamedTemporaryFile(buffering=0) - - -def divide_chunks(data: bytes, chunk_size: int) -> Iterator[bytes]: - """Chunk bytes data into smaller chunks.""" - for i in range(0, len(data), chunk_size): - yield data[i : i + chunk_size] - - -def get_primary_ip_address_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - for address in discovery_info.parsed_addresses(IPVersion.V4Only): - if address.startswith("127"): - # filter out loopback address - continue - if address.startswith("169.254"): - # filter out APIPA address - continue - return address - return None - - -def get_port_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - return discovery_info.port - - -async def close_async_generator(agen: AsyncGenerator[Any, None]) -> None: - """Force close an async generator.""" - task = asyncio.create_task(agen.__anext__()) - task.cancel() - with suppress(asyncio.CancelledError): - await task - await agen.aclose() - - -async def detect_charset(data: bytes, fallback="utf-8") -> str: - """Detect charset of raw data.""" - try: - detected = await asyncio.to_thread(chardet.detect, data) - if detected and detected["encoding"] and detected["confidence"] > 0.75: - return detected["encoding"] - except Exception as err: - LOGGER.debug("Failed to detect charset: %s", err) - return fallback - - -class TaskManager: - """ - Helper class to run many tasks at once. - - This is basically an alternative to asyncio.TaskGroup but this will not - cancel all operations when one of the tasks fails. - Logging of exceptions is done by the mass.create_task helper. - """ - - def __init__(self, mass: MusicAssistant, limit: int = 0): - """Initialize the TaskManager.""" - self.mass = mass - self._tasks: list[asyncio.Task] = [] - self._semaphore = asyncio.Semaphore(limit) if limit else None - - def create_task(self, coro: Coroutine) -> asyncio.Task: - """Create a new task and add it to the manager.""" - task = self.mass.create_task(coro) - self._tasks.append(task) - return task - - async def create_task_with_limit(self, coro: Coroutine) -> None: - """Create a new task with semaphore limit.""" - assert self._semaphore is not None - - def task_done_callback(_task: asyncio.Task) -> None: - self._tasks.remove(task) - self._semaphore.release() - - await self._semaphore.acquire() - task: asyncio.Task = self.create_task(coro) - task.add_done_callback(task_done_callback) - - async def __aenter__(self) -> Self: - """Enter context manager.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - if len(self._tasks) > 0: - await asyncio.wait(self._tasks) - self._tasks.clear() - - -_R = TypeVar("_R") -_P = ParamSpec("_P") - - -def lock( - func: Callable[_P, Awaitable[_R]], -) -> Callable[_P, Coroutine[Any, Any, _R]]: - """Call async function using a Lock.""" - - @functools.wraps(func) - async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: - """Call async function using the throttler with retries.""" - if not (func_lock := getattr(func, "lock", None)): - func_lock = asyncio.Lock() - func.lock = func_lock - async with func_lock: - return await func(*args, **kwargs) - - return wrapper - - -class TimedAsyncGenerator: - """ - Async iterable that times out after a given time. - - Source: https://medium.com/@dmitry8912/implementing-timeouts-in-pythons-asynchronous-generators-f7cbaa6dc1e9 - """ - - def __init__(self, iterable, timeout=0): - """ - Initialize the AsyncTimedIterable. - - Args: - iterable: The async iterable to wrap. - timeout: The timeout in seconds for each iteration. - """ - - class AsyncTimedIterator: - def __init__(self): - self._iterator = iterable.__aiter__() - - async def __anext__(self): - result = await asyncio.wait_for(self._iterator.__anext__(), int(timeout)) - if not result: - raise StopAsyncIteration - return result - - self._factory = AsyncTimedIterator - - def __aiter__(self): - """Return the async iterator.""" - return self._factory() diff --git a/music_assistant/server/helpers/webserver.py b/music_assistant/server/helpers/webserver.py deleted file mode 100644 index 249df4fe..00000000 --- a/music_assistant/server/helpers/webserver.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Base Webserver logic for an HTTPServer that can handle dynamic routes.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Final - -from aiohttp import web - -if TYPE_CHECKING: - import logging - from collections.abc import Awaitable, Callable - -MAX_CLIENT_SIZE: Final = 1024**2 * 16 -MAX_LINE_SIZE: Final = 24570 - - -class Webserver: - """Base Webserver logic for an HTTPServer that can handle dynamic routes.""" - - def __init__( - self, - logger: logging.Logger, - enable_dynamic_routes: bool = False, - ) -> None: - """Initialize instance.""" - self.logger = logger - # the below gets initialized in async setup - self._apprunner: web.AppRunner | None = None - self._webapp: web.Application | None = None - self._tcp_site: web.TCPSite | None = None - self._static_routes: list[tuple[str, str, Awaitable]] | None = None - self._dynamic_routes: dict[str, Callable] | None = {} if enable_dynamic_routes else None - self._bind_port: int | None = None - - async def setup( - self, - bind_ip: str | None, - bind_port: int, - base_url: str, - static_routes: list[tuple[str, str, Awaitable]] | None = None, - static_content: tuple[str, str, str] | None = None, - ) -> None: - """Async initialize of module.""" - self._base_url = base_url[:-1] if base_url.endswith("/") else base_url - self._bind_port = bind_port - self._static_routes = static_routes - self._webapp = web.Application( - logger=self.logger, - client_max_size=MAX_CLIENT_SIZE, - handler_args={ - "max_line_size": MAX_LINE_SIZE, - "max_field_size": MAX_LINE_SIZE, - }, - ) - self.logger.info("Starting server on %s:%s - base url: %s", bind_ip, bind_port, base_url) - self._apprunner = web.AppRunner(self._webapp, access_log=None, shutdown_timeout=10) - # add static routes - if self._static_routes: - for method, path, handler in self._static_routes: - self._webapp.router.add_route(method, path, handler) - if static_content: - self._webapp.router.add_static( - static_content[0], static_content[1], name=static_content[2] - ) - # register catch-all route to handle dynamic routes (if enabled) - if self._dynamic_routes is not None: - self._webapp.router.add_route("*", "/{tail:.*}", self._handle_catch_all) - await self._apprunner.setup() - # set host to None to bind to all addresses on both IPv4 and IPv6 - host = None if bind_ip == "0.0.0.0" else bind_ip - try: - self._tcp_site = web.TCPSite(self._apprunner, host=host, port=bind_port) - await self._tcp_site.start() - except OSError: - if host is None: - raise - # the configured interface is not available, retry on all interfaces - self.logger.error( - "Could not bind to %s, will start on all interfaces as fallback!", host - ) - self._tcp_site = web.TCPSite(self._apprunner, host=None, port=bind_port) - await self._tcp_site.start() - - async def close(self) -> None: - """Cleanup on exit.""" - # stop/clean webserver - await self._tcp_site.stop() - await self._apprunner.cleanup() - await self._webapp.shutdown() - await self._webapp.cleanup() - - @property - def base_url(self): - """Return the base URL of this webserver.""" - return self._base_url - - @property - def port(self): - """Return the port of this webserver.""" - return self._bind_port - - def register_dynamic_route(self, path: str, handler: Awaitable, method: str = "*") -> Callable: - """Register a dynamic route on the webserver, returns handler to unregister.""" - if self._dynamic_routes is None: - msg = "Dynamic routes are not enabled" - raise RuntimeError(msg) - key = f"{method}.{path}" - if key in self._dynamic_routes: - msg = f"Route {path} already registered." - raise RuntimeError(msg) - self._dynamic_routes[key] = handler - - def _remove(): - return self._dynamic_routes.pop(key) - - return _remove - - def unregister_dynamic_route(self, path: str, method: str = "*") -> None: - """Unregister a dynamic route from the webserver.""" - if self._dynamic_routes is None: - msg = "Dynamic routes are not enabled" - raise RuntimeError(msg) - key = f"{method}.{path}" - self._dynamic_routes.pop(key) - - async def serve_static(self, file_path: str, request: web.Request) -> web.FileResponse: - """Serve file response.""" - headers = {"Cache-Control": "no-cache"} - return web.FileResponse(file_path, headers=headers) - - async def _handle_catch_all(self, request: web.Request) -> web.Response: - """Redirect request to correct destination.""" - # find handler for the request - for key in (f"{request.method}.{request.path}", f"*.{request.path}"): - if handler := self._dynamic_routes.get(key): - return await handler(request) - # deny all other requests - self.logger.warning( - "Received unhandled %s request to %s from %s\nheaders: %s\n", - request.method, - request.path, - request.remote, - request.headers, - ) - return web.Response(status=404) diff --git a/music_assistant/server/models/__init__.py b/music_assistant/server/models/__init__.py deleted file mode 100644 index 4e20ea72..00000000 --- a/music_assistant/server/models/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Server specific/only models.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Protocol - -from .metadata_provider import MetadataProvider -from .music_provider import MusicProvider -from .player_provider import PlayerProvider -from .plugin import PluginProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - - -ProviderInstanceType = MetadataProvider | MusicProvider | PlayerProvider | PluginProvider - - -class ProviderModuleType(Protocol): - """Model for a provider module to support type hints.""" - - @staticmethod - async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig - ) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - - @staticmethod - async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ diff --git a/music_assistant/server/models/core_controller.py b/music_assistant/server/models/core_controller.py deleted file mode 100644 index 3965b89a..00000000 --- a/music_assistant/server/models/core_controller.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Model/base for a Core controller within Music Assistant.""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import ProviderType -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - CoreConfig, - ) - from music_assistant.server import MusicAssistant - - -class CoreController: - """Base representation of a Core controller within Music Assistant.""" - - domain: str # used as identifier (=name of the module) - manifest: ProviderManifest # some info for the UI only - - def __init__(self, mass: MusicAssistant) -> None: - """Initialize MusicProvider.""" - self.mass = mass - self._set_logger() - self.manifest = ProviderManifest( - type=ProviderType.CORE, - domain=self.domain, - name=f"{self.domain.title()} Core controller", - description=f"{self.domain.title()} Core controller", - codeowners=["@music-assistant"], - icon="puzzle-outline", - ) - - async def get_config_entries( - self, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """Return all Config Entries for this core module (if any).""" - return () - - async def setup(self, config: CoreConfig) -> None: - """Async initialize of module.""" - - async def close(self) -> None: - """Handle logic on server stop.""" - - async def reload(self, config: CoreConfig | None = None) -> None: - """Reload this core controller.""" - await self.close() - if config is None: - config = await self.mass.config.get_core_config(self.domain) - log_level = config.get_value(CONF_LOG_LEVEL) - self._set_logger(log_level) - await self.setup(config) - - def _set_logger(self, log_level: str | None = None) -> None: - """Set the logger settings.""" - mass_logger = logging.getLogger(MASS_LOGGER_NAME) - self.logger = mass_logger.getChild(self.domain) - if log_level is None: - log_level = self.mass.config.get_raw_core_config_value( - self.domain, CONF_LOG_LEVEL, "GLOBAL" - ) - if log_level == "GLOBAL": - self.logger.setLevel(mass_logger.level) - else: - self.logger.setLevel(log_level) - if logging.getLogger().level > self.logger.level: - # if the root logger's level is higher, we need to adjust that too - logging.getLogger().setLevel(self.logger.level) diff --git a/music_assistant/server/models/metadata_provider.py b/music_assistant/server/models/metadata_provider.py deleted file mode 100644 index f19432fe..00000000 --- a/music_assistant/server/models/metadata_provider.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Model/base for a Metadata Provider implementation.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import ProviderFeature - -from .provider import Provider - -if TYPE_CHECKING: - from music_assistant.common.models.media_items import Album, Artist, MediaItemMetadata, Track - -# ruff: noqa: ARG001, ARG002 - -DEFAULT_SUPPORTED_FEATURES = ( - ProviderFeature.ARTIST_METADATA, - ProviderFeature.ALBUM_METADATA, - ProviderFeature.TRACK_METADATA, -) - - -class MetadataProvider(Provider): - """Base representation of a Metadata Provider (controller). - - Metadata Provider implementations should inherit from this base model. - """ - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return DEFAULT_SUPPORTED_FEATURES - - async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: - """Retrieve metadata for an artist on this Metadata provider.""" - if ProviderFeature.ARTIST_METADATA in self.supported_features: - raise NotImplementedError - - async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: - """Retrieve metadata for an album on this Metadata provider.""" - if ProviderFeature.ALBUM_METADATA in self.supported_features: - raise NotImplementedError - - async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None: - """Retrieve metadata for a track on this Metadata provider.""" - if ProviderFeature.TRACK_METADATA in self.supported_features: - raise NotImplementedError - - async def resolve_image(self, path: str) -> str | bytes: - """ - Resolve an image from an image path. - - This either returns (a generator to get) raw bytes of the image or - a string with an http(s) URL or local path that is accessible from the server. - """ - return path diff --git a/music_assistant/server/models/music_provider.py b/music_assistant/server/models/music_provider.py deleted file mode 100644 index 79e49e93..00000000 --- a/music_assistant/server/models/music_provider.py +++ /dev/null @@ -1,565 +0,0 @@ -"""Model/base for a Music Provider implementation.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Sequence -from typing import TYPE_CHECKING, cast - -from music_assistant.common.models.enums import CacheCategory, MediaType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError, MusicAssistantError -from music_assistant.common.models.media_items import ( - Album, - Artist, - BrowseFolder, - ItemMapping, - MediaItemType, - Playlist, - Radio, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails - -from .provider import Provider - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - -# ruff: noqa: ARG001, ARG002 - - -class MusicProvider(Provider): - """Base representation of a Music Provider (controller). - - Music Provider implementations should inherit from this base model. - """ - - @property - def is_streaming_provider(self) -> bool: - """ - Return True if the provider is a streaming provider. - - This literally means that the catalog is not the same as the library contents. - For local based providers (files, plex), the catalog is the same as the library content. - It also means that data is if this provider is NOT a streaming provider, - data cross instances is unique, the catalog and library differs per instance. - - Setting this to True will only query one instance of the provider for search and lookups. - Setting this to False will query all instances of this provider for search and lookups. - """ - return True - - @property - def lookup_key(self) -> str: - """Return domain if (multi-instance) streaming_provider or instance_id otherwise.""" - if self.is_streaming_provider or not self.manifest.multi_instance: - return self.domain - return self.instance_id - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - self.mass.music.start_sync(providers=[self.instance_id]) - - async def search( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 5, - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - if ProviderFeature.SEARCH in self.supported_features: - raise NotImplementedError - return SearchResults() - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve library artists from the provider.""" - if ProviderFeature.LIBRARY_ARTISTS in self.supported_features: - raise NotImplementedError - yield # type: ignore - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve library albums from the provider.""" - if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: - raise NotImplementedError - yield # type: ignore - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - if ProviderFeature.LIBRARY_TRACKS in self.supported_features: - raise NotImplementedError - yield # type: ignore - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve library/subscribed playlists from the provider.""" - if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: - raise NotImplementedError - yield # type: ignore - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - if ProviderFeature.LIBRARY_RADIOS in self.supported_features: - raise NotImplementedError - yield # type: ignore - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - raise NotImplementedError - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get a list of all albums for the given artist.""" - if ProviderFeature.ARTIST_ALBUMS in self.supported_features: - raise NotImplementedError - return [] - - async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: - """Get a list of most popular tracks for the given artist.""" - if ProviderFeature.ARTIST_TOPTRACKS in self.supported_features: - raise NotImplementedError - return [] - - async def get_album(self, prov_album_id: str) -> Album: # type: ignore[return] - """Get full album details by id.""" - if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: - raise NotImplementedError - - async def get_track(self, prov_track_id: str) -> Track: # type: ignore[return] - """Get full track details by id.""" - if ProviderFeature.LIBRARY_TRACKS in self.supported_features: - raise NotImplementedError - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[return] - """Get full playlist details by id.""" - if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: - raise NotImplementedError - - async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] - """Get full radio details by id.""" - if ProviderFeature.LIBRARY_RADIOS in self.supported_features: - raise NotImplementedError - - async def get_album_tracks( - self, - prov_album_id: str, # type: ignore[return] - ) -> list[Track]: - """Get album tracks for given album id.""" - if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: - raise NotImplementedError - - async def get_playlist_tracks( - self, - prov_playlist_id: str, - page: int = 0, - ) -> list[Track]: - """Get all playlist tracks for given playlist id.""" - if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: - raise NotImplementedError - - async def library_add(self, item: MediaItemType) -> bool: - """Add item to provider's library. Return true on success.""" - if ( - item.media_type == MediaType.ARTIST - and ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - item.media_type == MediaType.ALBUM - and ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - item.media_type == MediaType.TRACK - and ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - item.media_type == MediaType.PLAYLIST - and ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - item.media_type == MediaType.RADIO - and ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features - ): - raise NotImplementedError - self.logger.info( - "Provider %s does not support library edit, " - "the action will only be performed in the local database.", - self.name, - ) - return True - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove item from provider's library. Return true on success.""" - if ( - media_type == MediaType.ARTIST - and ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - media_type == MediaType.ALBUM - and ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - media_type == MediaType.TRACK - and ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - media_type == MediaType.PLAYLIST - and ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features - ): - raise NotImplementedError - if ( - media_type == MediaType.RADIO - and ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features - ): - raise NotImplementedError - self.logger.info( - "Provider %s does not support library edit, " - "the action will only be performed in the local database.", - self.name, - ) - return True - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - if ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: - raise NotImplementedError - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - if ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: - raise NotImplementedError - - async def create_playlist(self, name: str) -> Playlist: # type: ignore[return] - """Create a new playlist on provider with given name.""" - if ProviderFeature.PLAYLIST_CREATE in self.supported_features: - raise NotImplementedError - - async def get_similar_tracks( # type: ignore[return] - self, prov_track_id: str, limit: int = 25 - ) -> list[Track]: - """Retrieve a dynamic list of similar tracks based on the provided track.""" - if ProviderFeature.SIMILAR_TRACKS in self.supported_features: - raise NotImplementedError - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track/radio.""" - raise NotImplementedError - - async def get_audio_stream( # type: ignore[return] - self, streamdetails: StreamDetails, seek_position: int = 0 - ) -> AsyncGenerator[bytes, None]: - """ - Return the (custom) audio stream for the provider item. - - Will only be called when the stream_type is set to CUSTOM. - """ - if False: - yield - raise NotImplementedError - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - - async def resolve_image(self, path: str) -> str | bytes: - """ - Resolve an image from an image path. - - This either returns (a generator to get) raw bytes of the image or - a string with an http(s) URL or local path that is accessible from the server. - """ - return path - - async def get_item(self, media_type: MediaType, prov_item_id: str) -> MediaItemType: - """Get single MediaItem from provider.""" - if media_type == MediaType.ARTIST: - return await self.get_artist(prov_item_id) - if media_type == MediaType.ALBUM: - return await self.get_album(prov_item_id) - if media_type == MediaType.PLAYLIST: - return await self.get_playlist(prov_item_id) - if media_type == MediaType.RADIO: - return await self.get_radio(prov_item_id) - return await self.get_track(prov_item_id) - - async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: - """Browse this provider's items. - - :param path: The path to browse, (e.g. provider_id://artists). - """ - if ProviderFeature.BROWSE not in self.supported_features: - # we may NOT use the default implementation if the provider does not support browse - raise NotImplementedError - - subpath = path.split("://", 1)[1] - # this reference implementation can be overridden with a provider specific approach - if subpath == "artists": - library_items = await self.mass.cache.get( - "artist", - default=[], - category=CacheCategory.LIBRARY_ITEMS, - base_key=self.instance_id, - ) - library_items = cast(list[int], library_items) - query = "artists.item_id in :ids" - query_params = {"ids": library_items} - return await self.mass.music.artists.library_items( - provider=self.instance_id, extra_query=query, extra_query_params=query_params - ) - if subpath == "albums": - library_items = await self.mass.cache.get( - "album", - default=[], - category=CacheCategory.LIBRARY_ITEMS, - base_key=self.instance_id, - ) - library_items = cast(list[int], library_items) - query = "albums.item_id in :ids" - query_params = {"ids": library_items} - return await self.mass.music.albums.library_items( - extra_query=query, extra_query_params=query_params - ) - if subpath == "tracks": - library_items = await self.mass.cache.get( - "track", - default=[], - category=CacheCategory.LIBRARY_ITEMS, - base_key=self.instance_id, - ) - library_items = cast(list[int], library_items) - query = "tracks.item_id in :ids" - query_params = {"ids": library_items} - return await self.mass.music.tracks.library_items( - extra_query=query, extra_query_params=query_params - ) - if subpath == "radios": - library_items = await self.mass.cache.get( - "radio", - default=[], - category=CacheCategory.LIBRARY_ITEMS, - base_key=self.instance_id, - ) - library_items = cast(list[int], library_items) - query = "radios.item_id in :ids" - query_params = {"ids": library_items} - return await self.mass.music.radio.library_items( - extra_query=query, extra_query_params=query_params - ) - if subpath == "playlists": - library_items = await self.mass.cache.get( - "playlist", - default=[], - category=CacheCategory.LIBRARY_ITEMS, - base_key=self.instance_id, - ) - library_items = cast(list[int], library_items) - query = "playlists.item_id in :ids" - query_params = {"ids": library_items} - return await self.mass.music.playlists.library_items( - extra_query=query, extra_query_params=query_params - ) - if subpath: - # unknown path - msg = "Invalid subpath" - raise KeyError(msg) - - # no subpath: return main listing - items: list[MediaItemType] = [] - if ProviderFeature.LIBRARY_ARTISTS in self.supported_features: - items.append( - BrowseFolder( - item_id="artists", - provider=self.domain, - path=path + "artists", - name="", - label="artists", - ) - ) - if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: - items.append( - BrowseFolder( - item_id="albums", - provider=self.domain, - path=path + "albums", - name="", - label="albums", - ) - ) - if ProviderFeature.LIBRARY_TRACKS in self.supported_features: - items.append( - BrowseFolder( - item_id="tracks", - provider=self.domain, - path=path + "tracks", - name="", - label="tracks", - ) - ) - if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: - items.append( - BrowseFolder( - item_id="playlists", - provider=self.domain, - path=path + "playlists", - name="", - label="playlists", - ) - ) - if ProviderFeature.LIBRARY_RADIOS in self.supported_features: - items.append( - BrowseFolder( - item_id="radios", - provider=self.domain, - path=path + "radios", - name="", - label="radios", - ) - ) - return items - - async def recommendations(self) -> list[MediaItemType]: - """Get this provider's recommendations. - - Returns a actual and personalised list of Media items with recommendations - form this provider for the user/account. It may return nested levels with - BrowseFolder items. - """ - if ProviderFeature.RECOMMENDATIONS in self.supported_features: - raise NotImplementedError - return [] - - async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: - """Run library sync for this provider.""" - # this reference implementation can be overridden - # with a provider specific approach if needed - for media_type in media_types: - if not self.library_supported(media_type): - continue - self.logger.debug("Start sync of %s items.", media_type.value) - controller = self.mass.music.get_controller(media_type) - cur_db_ids = set() - async for prov_item in self._get_library_gen(media_type): - library_item = await controller.get_library_item_by_prov_mappings( - prov_item.provider_mappings, - ) - try: - if not library_item and not prov_item.available: - # skip unavailable tracks - self.logger.debug( - "Skipping sync of item %s because it is unavailable", prov_item.uri - ) - continue - if not library_item: - # create full db item - # note that we skip the metadata lookup purely to speed up the sync - # the additional metadata is then lazy retrieved afterwards - if self.is_streaming_provider: - prov_item.favorite = True - library_item = await controller.add_item_to_library(prov_item) - elif getattr(library_item, "cache_checksum", None) != getattr( - prov_item, "cache_checksum", None - ): - # existing dbitem checksum changed (playlists only) - library_item = await controller.update_item_in_library( - library_item.item_id, prov_item - ) - elif library_item.available != prov_item.available: - # existing item availability changed - library_item = await controller.update_item_in_library( - library_item.item_id, prov_item - ) - cur_db_ids.add(library_item.item_id) - await asyncio.sleep(0) # yield to eventloop - except MusicAssistantError as err: - self.logger.warning( - "Skipping sync of item %s - error details: %s", prov_item.uri, str(err) - ) - - # process deletions (= no longer in library) - cache_category = CacheCategory.LIBRARY_ITEMS - cache_base_key = self.instance_id - - prev_library_items: list[int] | None - if prev_library_items := await self.mass.cache.get( - media_type.value, category=cache_category, base_key=cache_base_key - ): - for db_id in prev_library_items: - if db_id not in cur_db_ids: - try: - item = await controller.get_library_item(db_id) - except MediaNotFoundError: - # edge case: the item is already removed - continue - remaining_providers = { - x.provider_domain - for x in item.provider_mappings - if x.provider_domain != self.domain - } - if not remaining_providers and media_type != MediaType.ARTIST: - # this item is removed from the provider's library - # and we have no other providers attached to it - # it is safe to remove it from the MA library too - # note we skip artists here to prevent a recursive removal - # of all albums and tracks underneath this artist - await controller.remove_item_from_library(db_id) - else: - # otherwise: just unmark favorite - await controller.set_favorite(db_id, False) - await asyncio.sleep(0) # yield to eventloop - await self.mass.cache.set( - media_type.value, list(cur_db_ids), category=cache_category, base_key=cache_base_key - ) - - # DO NOT OVERRIDE BELOW - - def library_supported(self, media_type: MediaType) -> bool: - """Return if Library is supported for given MediaType on this provider.""" - if media_type == MediaType.ARTIST: - return ProviderFeature.LIBRARY_ARTISTS in self.supported_features - if media_type == MediaType.ALBUM: - return ProviderFeature.LIBRARY_ALBUMS in self.supported_features - if media_type == MediaType.TRACK: - return ProviderFeature.LIBRARY_TRACKS in self.supported_features - if media_type == MediaType.PLAYLIST: - return ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features - if media_type == MediaType.RADIO: - return ProviderFeature.LIBRARY_RADIOS in self.supported_features - return False - - def library_edit_supported(self, media_type: MediaType) -> bool: - """Return if Library add/remove is supported for given MediaType on this provider.""" - if media_type == MediaType.ARTIST: - return ProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features - if media_type == MediaType.ALBUM: - return ProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features - if media_type == MediaType.TRACK: - return ProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features - if media_type == MediaType.PLAYLIST: - return ProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features - if media_type == MediaType.RADIO: - return ProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features - return False - - def _get_library_gen(self, media_type: MediaType) -> AsyncGenerator[MediaItemType, None]: - """Return library generator for given media_type.""" - if media_type == MediaType.ARTIST: - return self.get_library_artists() - if media_type == MediaType.ALBUM: - return self.get_library_albums() - if media_type == MediaType.TRACK: - return self.get_library_tracks() - if media_type == MediaType.PLAYLIST: - return self.get_library_playlists() - if media_type == MediaType.RADIO: - return self.get_library_radios() - raise NotImplementedError diff --git a/music_assistant/server/models/player_provider.py b/music_assistant/server/models/player_provider.py deleted file mode 100644 index 51ba8229..00000000 --- a/music_assistant/server/models/player_provider.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Model/base for a Metadata Provider implementation.""" - -from __future__ import annotations - -from abc import abstractmethod - -from zeroconf import ServiceStateChange -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.models.config_entries import ( - BASE_PLAYER_CONFIG_ENTRIES, - CONF_ENTRY_ANNOUNCE_VOLUME, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, - ConfigEntry, - PlayerConfig, -) -from music_assistant.common.models.errors import UnsupportedFeaturedException -from music_assistant.common.models.player import Player, PlayerMedia - -from .provider import Provider - -# ruff: noqa: ARG001, ARG002 - - -class PlayerProvider(Provider): - """Base representation of a Player Provider (controller). - - Player Provider implementations should inherit from this base model. - """ - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - await self.discover_players() - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - return ( - *BASE_PLAYER_CONFIG_ENTRIES, - # add default entries for announce feature - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, - CONF_ENTRY_ANNOUNCE_VOLUME, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX, - ) - - async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: - """Call (by config manager) when the configuration of a player changes.""" - # default implementation: feel free to override - if ( - "enabled" in changed_keys - and config.enabled - and not self.mass.players.get(config.player_id) - ): - # if a player gets enabled, trigger discovery - task_id = f"discover_players_{self.instance_id}" - self.mass.call_later(5, self.discover_players, task_id=task_id) - else: - await self.poll_player(config.player_id) - - @abstractmethod - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player. - - - player_id: player_id of the player to handle the command. - """ - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY (unpause) command to given player. - - - player_id: player_id of the player to handle the command. - """ - # will only be called for players with Pause feature set. - raise NotImplementedError - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player. - - - player_id: player_id of the player to handle the command. - """ - # will only be called for players with Pause feature set. - raise NotImplementedError - - @abstractmethod - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player. - - This is called by the Players controller to start playing a mediaitem on the given player. - The provider's own implementation should work out how to handle this request. - - - player_id: player_id of the player to handle the command. - - media: Details of the item that needs to be played on the player. - """ - raise NotImplementedError - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """ - Handle enqueuing of the next (queue) item on the player. - - Called when player reports it started buffering a queue item - and when the queue items updated. - - A PlayerProvider implementation is in itself responsible for handling this - so that the queue items keep playing until its empty or the player stopped. - - This will NOT be called if the end of the queue is reached (and repeat disabled). - This will NOT be called if the player is using flow mode to playback the queue. - """ - # will only be called for players with ENQUEUE feature set. - raise NotImplementedError - - async def play_announcement( - self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None - ) -> None: - """Handle (provider native) playback of an announcement on given player.""" - # will only be called for players with PLAY_ANNOUNCEMENT feature set. - raise NotImplementedError - - async def cmd_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player. - - - player_id: player_id of the player to handle the command. - - powered: bool if player should be powered on or off. - """ - # will only be called for players with Power feature set. - raise NotImplementedError - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player. - - - player_id: player_id of the player to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - # will only be called for players with Volume feature set. - raise NotImplementedError - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player. - - - player_id: player_id of the player to handle the command. - - muted: bool if player should be muted. - """ - # will only be called for players with Mute feature set. - raise NotImplementedError - - async def cmd_seek(self, player_id: str, position: int) -> None: - """Handle SEEK command for given player. - - - player_id: player_id of the player to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - # will only be called for players with Seek feature set. - raise NotImplementedError - - async def cmd_next(self, player_id: str) -> None: - """Handle NEXT TRACK command for given player.""" - # will only be called for players with 'next_previous' feature set. - raise NotImplementedError - - async def cmd_previous(self, player_id: str) -> None: - """Handle PREVIOUS TRACK command for given player.""" - # will only be called for players with 'next_previous' feature set. - raise NotImplementedError - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the sync leader. - """ - # will only be called for players with SYNC feature set. - raise NotImplementedError - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - # will only be called for players with SYNC feature set. - raise NotImplementedError - - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - for child_id in child_player_ids: - # default implementation, simply call the cmd_sync for all child players - await self.cmd_sync(child_id, target_player) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates. - - This is called by the Player Manager; - if 'needs_poll' is set to True in the player object. - """ - - async def remove_player(self, player_id: str) -> None: - """Remove a player.""" - # will only be called for players with REMOVE_PLAYER feature set. - raise NotImplementedError - - async def discover_players(self) -> None: - """Discover players for this provider.""" - # This will be called (once) when the player provider is loaded into MA. - # Default implementation is mdns discovery, which will also automatically - # discovery players during runtime. If a provider overrides this method and - # doesn't use mdns, it is responsible for periodically searching for new players. - if not self.available: - return - for mdns_type in self.manifest.mdns_discovery or []: - for mdns_name in set(self.mass.aiozc.zeroconf.cache.cache): - if mdns_type not in mdns_name or mdns_type == mdns_name: - continue - info = AsyncServiceInfo(mdns_type, mdns_name) - if await info.async_request(self.mass.aiozc.zeroconf, 3000): - await self.on_mdns_service_state_change( - mdns_name, ServiceStateChange.Added, info - ) - - async def set_members(self, player_id: str, members: list[str]) -> None: - """Set members for a groupplayer.""" - # will only be called for (group)players with SET_MEMBERS feature set. - raise UnsupportedFeaturedException - - # DO NOT OVERRIDE BELOW - - @property - def players(self) -> list[Player]: - """Return all players belonging to this provider.""" - return [ - player - for player in self.mass.players - if player.provider in (self.instance_id, self.domain) - ] diff --git a/music_assistant/server/models/plugin.py b/music_assistant/server/models/plugin.py deleted file mode 100644 index 8c0ebc5e..00000000 --- a/music_assistant/server/models/plugin.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Model/base for a Plugin Provider implementation.""" - -from __future__ import annotations - -from .provider import Provider - -# ruff: noqa: ARG001, ARG002 - - -class PluginProvider(Provider): - """ - Base representation of a Plugin for Music Assistant. - - Plugin Provider implementations should inherit from this base model. - """ diff --git a/music_assistant/server/models/provider.py b/music_assistant/server/models/provider.py deleted file mode 100644 index dc415a4c..00000000 --- a/music_assistant/server/models/provider.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Model/base for a Provider implementation within Music Assistant.""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Any - -from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME - -if TYPE_CHECKING: - from zeroconf import ServiceStateChange - from zeroconf.asyncio import AsyncServiceInfo - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.enums import ProviderFeature, ProviderType - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - - -class Provider: - """Base representation of a Provider implementation within Music Assistant.""" - - def __init__( - self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig - ) -> None: - """Initialize MusicProvider.""" - self.mass = mass - self.manifest = manifest - self.config = config - mass_logger = logging.getLogger(MASS_LOGGER_NAME) - self.logger = mass_logger.getChild(self.domain) - log_level = config.get_value(CONF_LOG_LEVEL) - if log_level == "GLOBAL": - self.logger.setLevel(mass_logger.level) - else: - self.logger.setLevel(log_level) - if logging.getLogger().level > self.logger.level: - # if the root logger's level is higher, we need to adjust that too - logging.getLogger().setLevel(self.logger.level) - self.logger.debug("Log level configured to %s", log_level) - self.cache = mass.cache - self.available = False - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return () - - @property - def lookup_key(self) -> str: - """Return instance_id if multi_instance capable or domain otherwise.""" - # should not be overridden in normal circumstances - return self.instance_id if self.manifest.multi_instance else self.domain - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - - async def on_mdns_service_state_change( - self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None - ) -> None: - """Handle MDNS service state callback.""" - - @property - def type(self) -> ProviderType: - """Return type of this provider.""" - return self.manifest.type - - @property - def domain(self) -> str: - """Return domain for this provider.""" - return self.manifest.domain - - @property - def instance_id(self) -> str: - """Return instance_id for this provider(instance).""" - return self.config.instance_id - - @property - def name(self) -> str: - """Return (custom) friendly name for this provider instance.""" - if self.config.name: - return self.config.name - inst_count = len([x for x in self.mass.music.providers if x.domain == self.domain]) - if inst_count > 1: - postfix = self.instance_id[:-8] - return f"{self.manifest.name}.{postfix}" - return self.manifest.name - - def to_dict(self, *args, **kwargs) -> dict[str, Any]: - """Return Provider(instance) as serializable dict.""" - return { - "type": self.type.value, - "domain": self.domain, - "name": self.config.name or self.name, - "instance_id": self.instance_id, - "lookup_key": self.lookup_key, - "supported_features": [x.value for x in self.supported_features], - "available": self.available, - "is_streaming_provider": getattr(self, "is_streaming_provider", None), - } diff --git a/music_assistant/server/providers/__init__.py b/music_assistant/server/providers/__init__.py deleted file mode 100644 index 3a28df8a..00000000 --- a/music_assistant/server/providers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package with Music Provider controllers.""" diff --git a/music_assistant/server/providers/_template_music_provider/__init__.py b/music_assistant/server/providers/_template_music_provider/__init__.py deleted file mode 100644 index ee8a0cee..00000000 --- a/music_assistant/server/providers/_template_music_provider/__init__.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -DEMO/TEMPLATE Music Provider for Music Assistant. - -This is an empty music provider with no actual implementation. -Its meant to get started developing a new music provider for Music Assistant. - -Use it as a reference to discover what methods exists and what they should return. -Also it is good to look at existing music providers to get a better understanding, -due to the fact that providers may be flexible and support different features. - -If you are relying on a third-party library to interact with the music source, -you can then reference your library in the manifest in the requirements section, -which is a list of (versioned!) python modules (pip syntax) that should be installed -when the provider is selected by the user. - -Please keep in mind that Music Assistant is a fully async application and all -methods should be implemented as async methods. If you are not familiar with -async programming in Python, we recommend you to read up on it first. -If you are using a third-party library that is not async, you can need to use the several -helper methods such as asyncio.to_thread or the create_task in the mass object to wrap -the calls to the library in a thread. - -To add a new provider to Music Assistant, you need to create a new folder -in the providers folder with the name of your provider (e.g. 'my_music_provider'). -In that folder you should create (at least) a __init__.py file and a manifest.json file. - -Optional is an icon.svg file that will be used as the icon for the provider in the UI, -but we also support that you specify a material design icon in the manifest.json file. - -IMPORTANT NOTE: -We strongly recommend developing on either MacOS or Linux and start your development -environment by running the setup.sh script in the scripts folder of the repository. -This will create a virtual environment and install all dependencies needed for development. -See also our general DEVELOPMENT.md guide in the repository for more information. - -""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator, Sequence -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ContentType, MediaType, ProviderFeature, StreamType -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - ItemMapping, - MediaItemType, - Playlist, - ProviderMapping, - Radio, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - # setup is called when the user wants to setup a new provider instance. - # you are free to do any preflight checks here and but you must return - # an instance of the provider. - return MyDemoMusicprovider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - # Config Entries are used to configure the Music Provider if needed. - # See the models of ConfigEntry and ConfigValueType for more information what is supported. - # The ConfigEntry is a dataclass that represents a single configuration entry. - # The ConfigValueType is an Enum that represents the type of value that - # can be stored in a ConfigEntry. - # If your provider does not need any configuration, you can return an empty tuple. - - # We support flow-like configuration where you can have multiple steps of configuration - # using the 'action' parameter to distinguish between the different steps. - # The 'values' parameter contains the raw values of the config entries that were filled in - # by the user in the UI. This is a dictionary with the key being the config entry id - # and the value being the actual value filled in by the user. - - # For authentication flows where the user needs to be redirected to a login page - # or some other external service, we have a simple helper that can help you with those steps - # and a callback url that you can use to redirect the user back to the Music Assistant UI. - # See for example the Deezer provider for an example of how to use this. - return () - - -class MyDemoMusicprovider(MusicProvider): - """ - Example/demo Music provider. - - Note that this is always subclassed from MusicProvider, - which in turn is a subclass of the generic Provider model. - - The base implementation already takes care of some convenience methods, - such as the mass object and the logger. Take a look at the base class - for more information on what is available. - - Just like with any other subclass, make sure that if you override - any of the default methods (such as __init__), you call the super() method. - In most cases its not needed to override any of the builtin methods and you only - implement the abc methods with your actual implementation. - """ - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - # MANDATORY - # you should return a tuple of provider-level features - # here that your player provider supports or an empty tuple if none. - # for example 'ProviderFeature.SYNC_PLAYERS' if you can sync players. - return ( - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.RECOMMENDATIONS, - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.SIMILAR_TRACKS, - # see the ProviderFeature enum for all available features - ) - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - # OPTIONAL - # this is an optional method that you can implement if - # relevant or leave out completely if not needed. - # In most cases this can be omitted for music providers. - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - # OPTIONAL - # This is an optional method that you can implement if - # relevant or leave out completely if not needed. - # It will be called when the provider is unloaded from Music Assistant. - # for example to disconnect from a service or clean up resources. - - @property - def is_streaming_provider(self) -> bool: - """ - Return True if the provider is a streaming provider. - - This literally means that the catalog is not the same as the library contents. - For local based providers (files, plex), the catalog is the same as the library content. - It also means that data is if this provider is NOT a streaming provider, - data cross instances is unique, the catalog and library differs per instance. - - Setting this to True will only query one instance of the provider for search and lookups. - Setting this to False will query all instances of this provider for search and lookups. - """ - # For streaming providers return True here but for local file based providers return False. - return True - - async def search( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 5, - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - # OPTIONAL - # Will only be called if you reported the SEARCH feature in the supported_features. - # It allows searching your provider for media items. - # See the model for SearchResults for more information on what to return, but - # in general you should return a list of MediaItems for each media type. - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve library artists from the provider.""" - # OPTIONAL - # Will only be called if you reported the LIBRARY_ARTISTS feature - # in the supported_features and you did not override the default sync method. - # It allows retrieving the library/favorite artists from your provider. - # Warning: Async generator: - # You should yield Artist objects for each artist in the library. - yield Artist( - # A simple example of an artist object, - # you should replace this with actual data from your provider. - # Explore the Artist model for all options and descriptions. - item_id="123", - provider=self.instance_id, - name="Artist Name", - provider_mappings={ - ProviderMapping( - # A provider mapping is used to provide details about this item on this provider - # Music Assistant differentiates between domain and instance id to account for - # multiple instances of the same provider. - # The instance_id is auto generated by MA. - item_id="123", - provider_domain=self.domain, - provider_instance=self.instance_id, - # set 'available' to false if the item is (temporary) unavailable - available=True, - audio_format=AudioFormat( - # provide details here about sample rate etc. if known - content_type=ContentType.FLAC, - ), - ) - }, - ) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve library albums from the provider.""" - # OPTIONAL - # Will only be called if you reported the LIBRARY_ALBUMS feature - # in the supported_features and you did not override the default sync method. - # It allows retrieving the library/favorite albums from your provider. - # Warning: Async generator: - # You should yield Album objects for each album in the library. - yield # type: ignore - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - # OPTIONAL - # Will only be called if you reported the LIBRARY_TRACKS feature - # in the supported_features and you did not override the default sync method. - # It allows retrieving the library/favorite tracks from your provider. - # Warning: Async generator: - # You should yield Track objects for each track in the library. - yield # type: ignore - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve library/subscribed playlists from the provider.""" - # OPTIONAL - # Will only be called if you reported the LIBRARY_PLAYLISTS feature - # in the supported_features and you did not override the default sync method. - # It allows retrieving the library/favorite playlists from your provider. - # Warning: Async generator: - # You should yield Playlist objects for each playlist in the library. - yield # type: ignore - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - # OPTIONAL - # Will only be called if you reported the LIBRARY_RADIOS feature - # in the supported_features and you did not override the default sync method. - # It allows retrieving the library/favorite radio stations from your provider. - # Warning: Async generator: - # You should yield Radio objects for each radio station in the library. - yield - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - # Get full details of a single Artist. - # Mandatory only if you reported LIBRARY_ARTISTS in the supported_features. - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get a list of all albums for the given artist.""" - # Get a list of all albums for the given artist. - # Mandatory only if you reported ARTIST_ALBUMS in the supported_features. - - async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: - """Get a list of most popular tracks for the given artist.""" - # Get a list of most popular tracks for the given artist. - # Mandatory only if you reported ARTIST_TOPTRACKS in the supported_features. - # Note that (local) file based providers will simply return all artist tracks here. - - async def get_album(self, prov_album_id: str) -> Album: # type: ignore[return] - """Get full album details by id.""" - # Get full details of a single Album. - # Mandatory only if you reported LIBRARY_ALBUMS in the supported_features. - - async def get_track(self, prov_track_id: str) -> Track: # type: ignore[return] - """Get full track details by id.""" - # Get full details of a single Track. - # Mandatory only if you reported LIBRARY_TRACKS in the supported_features. - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[return] - """Get full playlist details by id.""" - # Get full details of a single Playlist. - # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported - - async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] - """Get full radio details by id.""" - # Get full details of a single Radio station. - # Mandatory only if you reported LIBRARY_RADIOS in the supported_features. - - async def get_album_tracks( - self, - prov_album_id: str, # type: ignore[return] - ) -> list[Track]: - """Get album tracks for given album id.""" - # Get all tracks for a given album. - # Mandatory only if you reported ARTIST_ALBUMS in the supported_features. - - async def get_playlist_tracks( - self, - prov_playlist_id: str, - page: int = 0, - ) -> list[Track]: - """Get all playlist tracks for given playlist id.""" - # Get all tracks for a given playlist. - # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported_features. - - async def library_add(self, item: MediaItemType) -> bool: - """Add item to provider's library. Return true on success.""" - # Add an item to your provider's library. - # This is only called if the provider supports the EDIT feature for the media type. - return True - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove item from provider's library. Return true on success.""" - # Remove an item from your provider's library. - # This is only called if the provider supports the EDIT feature for the media type. - return True - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - # Add track(s) to a playlist. - # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature. - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - # Remove track(s) from a playlist. - # This is only called if the provider supports the EDPLAYLIST_TRACKS_EDITIT feature. - - async def create_playlist(self, name: str) -> Playlist: # type: ignore[return] - """Create a new playlist on provider with given name.""" - # Create a new playlist on the provider. - # This is only called if the provider supports the PLAYLIST_CREATE feature. - - async def get_similar_tracks( # type: ignore[return] - self, prov_track_id: str, limit: int = 25 - ) -> list[Track]: - """Retrieve a dynamic list of similar tracks based on the provided track.""" - # Get a list of similar tracks based on the provided track. - # This is only called if the provider supports the SIMILAR_TRACKS feature. - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track/radio.""" - # Get stream details for a track or radio. - # Implementing this method is MANDATORY to allow playback. - # The StreamDetails contain info how Music Assistant can play the track. - # item_id will always be a track or radio id. Later, when/if MA supports - # podcasts or audiobooks, this may as well be an episode or chapter id. - # You should return a StreamDetails object here with the info as accurate as possible - # to allow Music Assistant to process the audio using ffmpeg. - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=AudioFormat( - # provide details here about sample rate etc. if known - # set content type to unknown to let ffmpeg guess the codec/container - content_type=ContentType.UNKNOWN, - ), - media_type=MediaType.TRACK, - # streamtype defines how the stream is provided - # for most providers this will be HTTP but you can also use CUSTOM - # to provide a custom stream generator in get_audio_stream. - stream_type=StreamType.HTTP, - # explore the StreamDetails model and StreamType enum for more options - # but the above should be the mandatory fields to set. - ) - - async def get_audio_stream( # type: ignore[return] - self, streamdetails: StreamDetails, seek_position: int = 0 - ) -> AsyncGenerator[bytes, None]: - """ - Return the (custom) audio stream for the provider item. - - Will only be called when the stream_type is set to CUSTOM. - """ - # this is an async generator that should yield raw audio bytes - # for the given streamdetails. You can use this to provide a custom - # stream generator for the audio stream. This is only called when the - # stream_type is set to CUSTOM in the get_stream_details method. - yield # type: ignore - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - # This is OPTIONAL callback that is called when an item has been streamed. - # You can use this e.g. for playback reporting or statistics. - - async def resolve_image(self, path: str) -> str | bytes: - """ - Resolve an image from an image path. - - This either returns (a generator to get) raw bytes of the image or - a string with an http(s) URL or local path that is accessible from the server. - """ - # This is an OPTIONAL method that you can implement to resolve image paths. - # This is used to resolve image paths that are returned in the MediaItems. - # You can return a URL to an image or a generator that yields the raw bytes of the image. - # This will only be called when you set 'remotely_accessible' - # to false in a MediaItemImage object. - return path - - async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: - """Browse this provider's items. - - :param path: The path to browse, (e.g. provider_id://artists). - """ - # Browse your provider's recommendations/media items. - # This is only called if you reported the BROWSE feature in the supported_features. - # You should return a list of MediaItems or ItemMappings for the given path. - # Note that you can return nested levels with BrowseFolder items. - - # The MusicProvider base model has a default implementation of this method - # that will call the get_library_* methods if you did not override it. - return [] - - async def recommendations(self) -> list[MediaItemType]: - """Get this provider's recommendations. - - Returns a actual and personalised list of Media items with recommendations - form this provider for the user/account. It may return nested levels with - BrowseFolder items. - """ - # Get this provider's recommendations. - # This is only called if you reported the RECOMMENDATIONS feature in the supported_features. - return [] - - async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: - """Run library sync for this provider.""" - # Run a full sync of the library for the given media types. - # This is called by the music controller to sync items from your provider to the library. - # As a generic rule of thumb the default implementation within the MusicProvider - # base model should be sufficient for most (streaming) providers. - # If you need to do some custom sync logic, you can override this method. - # For example the filesystem provider in MA, overrides this method to scan the filesystem. diff --git a/music_assistant/server/providers/_template_music_provider/icon.svg b/music_assistant/server/providers/_template_music_provider/icon.svg deleted file mode 100644 index 845920ca..00000000 --- a/music_assistant/server/providers/_template_music_provider/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/_template_music_provider/manifest.json b/music_assistant/server/providers/_template_music_provider/manifest.json deleted file mode 100644 index 15d6b83a..00000000 --- a/music_assistant/server/providers/_template_music_provider/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "template_player_provider", - "name": "Name of the Player provider goes here", - "description": "Short description of the player provider goes here", - "codeowners": ["@yourgithubusername"], - "requirements": [], - "documentation": "Link to the documentation on the music-assistant.io helppage (may be added later).", - "mdns_discovery": ["_optional_mdns_service_type._tcp.local."] -} diff --git a/music_assistant/server/providers/_template_player_provider/__init__.py b/music_assistant/server/providers/_template_player_provider/__init__.py deleted file mode 100644 index cf2b897c..00000000 --- a/music_assistant/server/providers/_template_player_provider/__init__.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -DEMO/TEMPLATE Player Provider for Music Assistant. - -This is an empty player provider with no actual implementation. -Its meant to get started developing a new player provider for Music Assistant. - -Use it as a reference to discover what methods exists and what they should return. -Also it is good to look at existing player providers to get a better understanding, -due to the fact that providers may be flexible and support different features and/or -ways to discover players on the network. - -In general, the actual device communication should reside in a separate library. -You can then reference your library in the manifest in the requirements section, -which is a list of (versioned!) python modules (pip syntax) that should be installed -when the provider is selected by the user. - -To add a new player provider to Music Assistant, you need to create a new folder -in the providers folder with the name of your provider (e.g. 'my_player_provider'). -In that folder you should create (at least) a __init__.py file and a manifest.json file. - -Optional is an icon.svg file that will be used as the icon for the provider in the UI, -but we also support that you specify a material design icon in the manifest.json file. - -IMPORTANT NOTE: -We strongly recommend developing on either MacOS or Linux and start your development -environment by running the setup.sh scripts in the scripts folder of the repository. -This will create a virtual environment and install all dependencies needed for development. -See also our general DEVELOPMENT.md guide in the repository for more information. - -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from zeroconf import ServiceStateChange - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType, PlayerConfig -from music_assistant.common.models.enums import PlayerFeature, PlayerType, ProviderFeature -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.util import get_primary_ip_address_from_zeroconf -from music_assistant.server.models.player_provider import PlayerProvider - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - # setup is called when the user wants to setup a new provider instance. - # you are free to do any preflight checks here and but you must return - # an instance of the provider. - return MyDemoPlayerprovider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - # Config Entries are used to configure the Player Provider if needed. - # See the models of ConfigEntry and ConfigValueType for more information what is supported. - # The ConfigEntry is a dataclass that represents a single configuration entry. - # The ConfigValueType is an Enum that represents the type of value that - # can be stored in a ConfigEntry. - # If your provider does not need any configuration, you can return an empty tuple. - return () - - -class MyDemoPlayerprovider(PlayerProvider): - """ - Example/demo Player provider. - - Note that this is always subclassed from PlayerProvider, - which in turn is a subclass of the generic Provider model. - - The base implementation already takes care of some convenience methods, - such as the mass object and the logger. Take a look at the base class - for more information on what is available. - - Just like with any other subclass, make sure that if you override - any of the default methods (such as __init__), you call the super() method. - In most cases its not needed to override any of the builtin methods and you only - implement the abc methods with your actual implementation. - """ - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - # MANDATORY - # you should return a tuple of provider-level features - # here that your player provider supports or an empty tuple if none. - # for example 'ProviderFeature.SYNC_PLAYERS' if you can sync players. - return (ProviderFeature.SYNC_PLAYERS,) - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - # OPTIONAL - # this is an optional method that you can implement if - # relevant or leave out completely if not needed. - # it will be called after the provider has been fully loaded into Music Assistant. - # you can use this for instance to trigger custom (non-mdns) discovery of players - # or any other logic that needs to run after the provider is fully loaded. - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - # OPTIONAL - # this is an optional method that you can implement if - # relevant or leave out completely if not needed. - # it will be called when the provider is unloaded from Music Assistant. - # this means also when the provider is getting reloaded - - async def on_mdns_service_state_change( - self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None - ) -> None: - """Handle MDNS service state callback.""" - # MANDATORY IF YOU WANT TO USE MDNS DISCOVERY - # OPTIONAL if you dont use mdns for discovery of players - # If you specify a mdns service type in the manifest.json, this method will be called - # automatically on mdns changes for the specified service type. - - # If no mdns service type is specified, this method is omitted and you - # can completely remove it from your provider implementation. - - # NOTE: If you do not use mdns for discovery of players on the network, - # you must implement your own discovery mechanism and logic to add new players - # and update them on state changes when needed. - # Below is a bit of example implementation but we advise to look at existing - # player providers for more inspiration. - name = name.split("@", 1)[1] if "@" in name else name - player_id = info.decoded_properties["uuid"] # this is just an example! - # handle removed player - if state_change == ServiceStateChange.Removed: - # check if the player manager has an existing entry for this player - if mass_player := self.mass.players.get(player_id): - # the player has become unavailable - self.logger.debug("Player offline: %s", mass_player.display_name) - mass_player.available = False - self.mass.players.update(player_id) - return - # handle update for existing device - # (state change is either updated or added) - # check if we have an existing player in the player manager - # note that you can use this point to update the player connection info - # if that changed (e.g. ip address) - if mass_player := self.mass.players.get(player_id): - # existing player found in the player manager, - # this is an existing player that has been updated/reconnected - # or simply a re-announcement on mdns. - cur_address = get_primary_ip_address_from_zeroconf(info) - if cur_address and cur_address != mass_player.device_info.address: - self.logger.debug( - "Address updated to %s for player %s", cur_address, mass_player.display_name - ) - mass_player.device_info = DeviceInfo( - model=mass_player.device_info.model, - manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), - ) - if not mass_player.available: - # if the player was marked offline and you now receive an mdns update - # it means the player is back online and we should try to connect to it - self.logger.debug("Player back online: %s", mass_player.display_name) - # you can try to connect to the player here if needed - mass_player.available = True - # inform the player manager of any changes to the player object - # note that you would normally call this from some other callback from - # the player's native api/library which informs you of changes in the player state. - # as a last resort you can also choose to let the player manager - # poll the player for state changes - self.mass.players.update(player_id) - return - # handle new player - self.logger.debug("Discovered device %s on %s", name, cur_address) - # your own connection logic will probably be implemented here where - # you connect to the player etc. using your device/provider specific library. - - # Instantiate the MA Player object and register it with the player manager - mass_player = Player( - player_id=player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=name, - available=True, - powered=False, - device_info=DeviceInfo( - model="Model XYX", - manufacturer="Super Brand", - address=cur_address, - ), - # set the supported features for this player only with - # the ones the player actually supports - supported_features=( - PlayerFeature.POWER, # if the player can be turned on/off - PlayerFeature.VOLUME_SET, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.PLAY_ANNOUNCEMENT, # see play_announcement method - ), - ) - # register the player with the player manager - await self.mass.players.register(mass_player) - - # once the player is registered, you can either instruct the player manager to - # poll the player for state changes or you can implement your own logic to - # listen for state changes from the player and update the player object accordingly. - # in any case, you need to call the update method on the player manager: - self.mass.players.update(player_id) - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - # OPTIONAL - # this method is optional and should be implemented if you need player specific - # configuration entries. If you do not need player specific configuration entries, - # you can leave this method out completely to accept the default implementation. - # Please note that you need to call the super() method to get the default entries. - return () - - async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: - """Call (by config manager) when the configuration of a player changes.""" - # OPTIONAL - # this will be called whenever a player config changes - # you can use this to react to changes in player configuration - # but this is completely optional and you can leave it out if not needed. - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - # MANDATORY - # this method is mandatory and should be implemented. - # this method should send a stop command to the given player. - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - # MANDATORY - # this method is mandatory and should be implemented. - # this method should send a play command to the given player. - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - # OPTIONAL - required only if you specified PlayerFeature.PAUSE - # this method should send a pause command to the given player. - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - # OPTIONAL - required only if you specified PlayerFeature.VOLUME_SET - # this method should send a volume set command to the given player. - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - # OPTIONAL - required only if you specified PlayerFeature.VOLUME_MUTE - # this method should send a volume mute command to the given player. - - async def cmd_seek(self, player_id: str, position: int) -> None: - """Handle SEEK command for given queue. - - - player_id: player_id of the player to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - # OPTIONAL - required only if you specified PlayerFeature.SEEK - # this method should handle the seek command for the given player. - # the position is the position in seconds to seek to in the current playing item. - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player. - - This is called by the Players controller to start playing a mediaitem on the given player. - The provider's own implementation should work out how to handle this request. - - - player_id: player_id of the player to handle the command. - - media: Details of the item that needs to be played on the player. - """ - # MANDATORY - # this method is mandatory and should be implemented. - # this method should handle the play_media command for the given player. - # It will be called when media needs to be played on the player. - # The media object contains all the details needed to play the item. - - # In 99% of the cases this will be called by the Queue controller to play - # a single item from the queue on the player and the uri within the media - # object will then contain the URL to play that single queue item. - - # If your player provider does not support enqueuing of items, - # the queue controller will simply call this play_media method for - # each item in the queue to play them one by one. - - # In order to support true gapless and/or crossfade, we offer the option of - # 'flow_mode' playback. In that case the queue controller will stitch together - # all songs in the playback queue into a single stream and send that to the player. - # In that case the URI (and metadata) received here is that of the 'flow mode' stream. - - # Examples of player providers that use flow mode for playback by default are Airplay, - # SnapCast and Fully Kiosk. - - # Examples of player providers that optionally use 'flow mode' are Google Cast and - # Home Assistant. They provide a config entry to enable flow mode playback. - - # Examples of player providers that natively support enqueuing of items are Sonos, - # Slimproto and Google Cast. - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """ - Handle enqueuing of the next (queue) item on the player. - - Called when player reports it started buffering a queue item - and when the queue items updated. - - A PlayerProvider implementation is in itself responsible for handling this - so that the queue items keep playing until its empty or the player stopped. - - This will NOT be called if the end of the queue is reached (and repeat disabled). - This will NOT be called if the player is using flow mode to playback the queue. - """ - # this method should handle the enqueuing of the next queue item on the player. - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - # OPTIONAL - required only if you specified ProviderFeature.SYNC_PLAYERS - # this method should handle the sync command for the given player. - # you should join the given player to the target_player/syncgroup. - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - sonos_player = self.sonos_players[player_id] - await sonos_player.client.player.leave_group() - - async def play_announcement( - self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None - ) -> None: - """Handle (provider native) playback of an announcement on given player.""" - # OPTIONAL - required only if you specified PlayerFeature.PLAY_ANNOUNCEMENT - # This method should handle the playback of an announcement on the given player. - # The announcement object contains all the details needed to play the announcement. - # The volume_level is optional and can be used to set the volume level for the announcement. - # If you do not use the announcement playerfeature, the default behavior is to play the - # announcement as a regular media item using the play_media method and the MA player manager - # will take care of setting the volume level for the announcement and resuming etc. - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - # OPTIONAL - # This method is optional and should be implemented if you specified 'needs_poll' - # on the Player object. This method should poll the player for state changes - # and update the player object in the player manager if needed. - # This method will be called at the interval specified in the poll_interval attribute. diff --git a/music_assistant/server/providers/_template_player_provider/icon.svg b/music_assistant/server/providers/_template_player_provider/icon.svg deleted file mode 100644 index 845920ca..00000000 --- a/music_assistant/server/providers/_template_player_provider/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/_template_player_provider/manifest.json b/music_assistant/server/providers/_template_player_provider/manifest.json deleted file mode 100644 index 15d6b83a..00000000 --- a/music_assistant/server/providers/_template_player_provider/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "template_player_provider", - "name": "Name of the Player provider goes here", - "description": "Short description of the player provider goes here", - "codeowners": ["@yourgithubusername"], - "requirements": [], - "documentation": "Link to the documentation on the music-assistant.io helppage (may be added later).", - "mdns_discovery": ["_optional_mdns_service_type._tcp.local."] -} diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py deleted file mode 100644 index 1a29387e..00000000 --- a/music_assistant/server/providers/airplay/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Airplay Player provider for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.server import MusicAssistant - -from .const import CONF_BIND_INTERFACE -from .provider import AirplayProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_BIND_INTERFACE, - type=ConfigEntryType.STRING, - default_value=mass.streams.publish_ip, - label="Bind interface", - description="Interface to bind to for Airplay streaming.", - category="advanced", - ), - ) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return AirplayProvider(mass, manifest, config) diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 b/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 deleted file mode 100755 index 21410d3f..00000000 Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 and /dev/null differ diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 b/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 deleted file mode 100755 index 95424e6f..00000000 Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 and /dev/null differ diff --git a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 b/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 deleted file mode 100755 index 0424e653..00000000 Binary files a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 and /dev/null differ diff --git a/music_assistant/server/providers/airplay/const.py b/music_assistant/server/providers/airplay/const.py deleted file mode 100644 index 3d9ecd8d..00000000 --- a/music_assistant/server/providers/airplay/const.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Constants for the AirPlay provider.""" - -from __future__ import annotations - -from music_assistant.common.models.enums import ContentType -from music_assistant.common.models.media_items import AudioFormat - -DOMAIN = "airplay" - -CONF_ENCRYPTION = "encryption" -CONF_ALAC_ENCODE = "alac_encode" -CONF_VOLUME_START = "volume_start" -CONF_PASSWORD = "password" -CONF_BIND_INTERFACE = "bind_interface" -CONF_READ_AHEAD_BUFFER = "read_ahead_buffer" - -BACKOFF_TIME_LOWER_LIMIT = 15 # seconds -BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes - -CONF_CREDENTIALS = "credentials" -CACHE_KEY_PREV_VOLUME = "airplay_prev_volume" -FALLBACK_VOLUME = 20 - -AIRPLAY_FLOW_PCM_FORMAT = AudioFormat( - content_type=ContentType.PCM_F32LE, - sample_rate=44100, - bit_depth=32, -) -AIRPLAY_PCM_FORMAT = AudioFormat( - content_type=ContentType.from_bit_depth(16), sample_rate=44100, bit_depth=16 -) diff --git a/music_assistant/server/providers/airplay/helpers.py b/music_assistant/server/providers/airplay/helpers.py deleted file mode 100644 index fe8f5180..00000000 --- a/music_assistant/server/providers/airplay/helpers.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Various helpers/utilities for the Airplay provider.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from zeroconf import IPVersion - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - -def convert_airplay_volume(value: float) -> int: - """Remap Airplay Volume to 0..100 scale.""" - airplay_min = -30 - airplay_max = 0 - normal_min = 0 - normal_max = 100 - portion = (value - airplay_min) * (normal_max - normal_min) / (airplay_max - airplay_min) - return int(portion + normal_min) - - -def get_model_from_am(am_property: str | None) -> tuple[str, str]: - """Return Manufacturer and Model name from mdns AM property.""" - manufacturer = "Unknown" - model = "Generic Airplay device" - if not am_property: - return (manufacturer, model) - if isinstance(am_property, bytes): - am_property = am_property.decode("utf-8") - if am_property == "AudioAccessory5,1": - model = "HomePod" - manufacturer = "Apple" - elif "AppleTV" in am_property: - model = "Apple TV" - manufacturer = "Apple" - else: - model = am_property - return (manufacturer, model) - - -def get_primary_ip_address(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - for address in discovery_info.parsed_addresses(IPVersion.V4Only): - if address.startswith("127"): - # filter out loopback address - continue - if address.startswith("169.254"): - # filter out APIPA address - continue - return address - return None diff --git a/music_assistant/server/providers/airplay/manifest.json b/music_assistant/server/providers/airplay/manifest.json deleted file mode 100644 index 3dbbbbb6..00000000 --- a/music_assistant/server/providers/airplay/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "player", - "domain": "airplay", - "name": "Airplay", - "description": "Support for players that support the Airplay protocol.", - "codeowners": [ - "@music-assistant" - ], - "requirements": [], - "documentation": "https://music-assistant.io/player-support/airplay/", - "multi_instance": false, - "builtin": false, - "icon": "cast-variant", - "mdns_discovery": [ - "_raop._tcp.local." - ] -} diff --git a/music_assistant/server/providers/airplay/player.py b/music_assistant/server/providers/airplay/player.py deleted file mode 100644 index db33b64f..00000000 --- a/music_assistant/server/providers/airplay/player.py +++ /dev/null @@ -1,50 +0,0 @@ -"""AirPlay Player definition.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import PlayerState - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - from .provider import AirplayProvider - from .raop import RaopStream - - -class AirPlayPlayer: - """Holds the details of the (discovered) Airplay (RAOP) player.""" - - def __init__( - self, prov: AirplayProvider, player_id: str, discovery_info: AsyncServiceInfo, address: str - ) -> None: - """Initialize AirPlayPlayer.""" - self.prov = prov - self.mass = prov.mass - self.player_id = player_id - self.discovery_info = discovery_info - self.address = address - self.logger = prov.logger.getChild(player_id) - self.raop_stream: RaopStream | None = None - self.last_command_sent = 0.0 - - async def cmd_stop(self, update_state: bool = True) -> None: - """Send STOP command to player.""" - if self.raop_stream: - # forward stop to the entire stream session - await self.raop_stream.session.stop() - if update_state and (mass_player := self.mass.players.get(self.player_id)): - mass_player.state = PlayerState.IDLE - self.mass.players.update(mass_player.player_id) - - async def cmd_play(self) -> None: - """Send PLAY (unpause) command to player.""" - if self.raop_stream and self.raop_stream.running: - await self.raop_stream.send_cli_command("ACTION=PLAY") - - async def cmd_pause(self) -> None: - """Send PAUSE command to player.""" - if not self.raop_stream or not self.raop_stream.running: - return - await self.raop_stream.send_cli_command("ACTION=PAUSE") diff --git a/music_assistant/server/providers/airplay/provider.py b/music_assistant/server/providers/airplay/provider.py deleted file mode 100644 index f7977cef..00000000 --- a/music_assistant/server/providers/airplay/provider.py +++ /dev/null @@ -1,637 +0,0 @@ -"""Airplay Player provider for Music Assistant.""" - -from __future__ import annotations - -import asyncio -import os -import platform -import socket -import time -from random import randrange -from typing import TYPE_CHECKING - -from zeroconf import ServiceStateChange -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.helpers.datetime import utc -from music_assistant.common.helpers.util import get_ip_pton, select_free_port -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_EQ_BASS, - CONF_ENTRY_EQ_MID, - CONF_ENTRY_EQ_TREBLE, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_OUTPUT_CHANNELS, - CONF_ENTRY_SYNC_ADJUST, - ConfigEntry, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - MediaType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.audio import get_ffmpeg_stream -from music_assistant.server.helpers.process import check_output -from music_assistant.server.helpers.util import TaskManager, lock -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.airplay.raop import RaopStreamSession - -from .const import ( - AIRPLAY_FLOW_PCM_FORMAT, - AIRPLAY_PCM_FORMAT, - CACHE_KEY_PREV_VOLUME, - CONF_ALAC_ENCODE, - CONF_ENCRYPTION, - CONF_PASSWORD, - CONF_READ_AHEAD_BUFFER, - FALLBACK_VOLUME, -) -from .helpers import convert_airplay_volume, get_model_from_am, get_primary_ip_address -from .player import AirPlayPlayer - -if TYPE_CHECKING: - from music_assistant.server.providers.player_group import PlayerGroupProvider - - -PLAYER_CONFIG_ENTRIES = ( - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_EQ_BASS, - CONF_ENTRY_EQ_MID, - CONF_ENTRY_EQ_TREBLE, - CONF_ENTRY_OUTPUT_CHANNELS, - ConfigEntry( - key=CONF_ENCRYPTION, - type=ConfigEntryType.BOOLEAN, - default_value=False, - label="Enable encryption", - description="Enable encrypted communication with the player, " - "some (3rd party) players require this.", - category="airplay", - ), - ConfigEntry( - key=CONF_ALAC_ENCODE, - type=ConfigEntryType.BOOLEAN, - default_value=True, - label="Enable compression", - description="Save some network bandwidth by sending the audio as " - "(lossless) ALAC at the cost of a bit CPU.", - category="airplay", - ), - CONF_ENTRY_SYNC_ADJUST, - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - default_value=None, - required=False, - label="Device password", - description="Some devices require a password to connect/play.", - category="airplay", - ), - ConfigEntry( - key=CONF_READ_AHEAD_BUFFER, - type=ConfigEntryType.INTEGER, - default_value=1000, - required=False, - label="Audio buffer (ms)", - description="Amount of buffer (in milliseconds), " - "the player should keep to absorb network throughput jitter. " - "If you experience audio dropouts, try increasing this value.", - category="airplay", - range=(500, 3000), - ), - # airplay has fixed sample rate/bit depth so make this config entry static and hidden - create_sample_rates_config_entry(44100, 16, 44100, 16, True), -) - - -# TODO: Airplay provider -# - Implement authentication for Apple TV -# - Implement volume control for Apple devices using pyatv -# - Implement metadata for Apple Apple devices using pyatv -# - Use pyatv for communicating with original Apple devices (and use cliraop for actual streaming) -# - Implement Airplay 2 support -# - Implement late joining to existing stream (instead of restarting it) - - -class AirplayProvider(PlayerProvider): - """Player provider for Airplay based players.""" - - cliraop_bin: str | None = None - _players: dict[str, AirPlayPlayer] - _dacp_server: asyncio.Server = None - _dacp_info: AsyncServiceInfo = None - _play_media_lock: asyncio.Lock = asyncio.Lock() - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self._players = {} - self.cliraop_bin = await self._getcliraop_binary() - dacp_port = await select_free_port(39831, 49831) - self.dacp_id = dacp_id = f"{randrange(2 ** 64):X}" - self.logger.debug("Starting DACP ActiveRemote %s on port %s", dacp_id, dacp_port) - self._dacp_server = await asyncio.start_server( - self._handle_dacp_request, "0.0.0.0", dacp_port - ) - zeroconf_type = "_dacp._tcp.local." - server_id = f"iTunes_Ctrl_{dacp_id}.{zeroconf_type}" - self._dacp_info = AsyncServiceInfo( - zeroconf_type, - name=server_id, - addresses=[await get_ip_pton(self.mass.streams.publish_ip)], - port=dacp_port, - properties={ - "txtvers": "1", - "Ver": "63B5E5C0C201542E", - "DbId": "63B5E5C0C201542E", - "OSsi": "0x1F5", - }, - server=f"{socket.gethostname()}.local", - ) - await self.mass.aiozc.async_register_service(self._dacp_info) - - async def on_mdns_service_state_change( - self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None - ) -> None: - """Handle MDNS service state callback.""" - raw_id, display_name = name.split(".")[0].split("@", 1) - player_id = f"ap{raw_id.lower()}" - # handle removed player - if state_change == ServiceStateChange.Removed: - if mass_player := self.mass.players.get(player_id): - if not mass_player.available: - return - # the player has become unavailable - self.logger.debug("Player offline: %s", display_name) - mass_player.available = False - self.mass.players.update(player_id) - return - # handle update for existing device - if airplay_player := self._players.get(player_id): - if mass_player := self.mass.players.get(player_id): - cur_address = get_primary_ip_address(info) - if cur_address and cur_address != airplay_player.address: - airplay_player.logger.debug( - "Address updated from %s to %s", airplay_player.address, cur_address - ) - airplay_player.address = cur_address - mass_player.device_info = DeviceInfo( - model=mass_player.device_info.model, - manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), - ) - if not mass_player.available: - self.logger.debug("Player back online: %s", display_name) - mass_player.available = True - # always update the latest discovery info - airplay_player.discovery_info = info - self.mass.players.update(player_id) - return - # handle new player - await self._setup_player(player_id, display_name, info) - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - # power off all players (will disconnect and close cliraop) - for player_id in self._players: - await self.cmd_power(player_id, False) - # shutdown DACP server - if self._dacp_server: - self._dacp_server.close() - # shutdown DACP zeroconf service - if self._dacp_info: - await self.mass.aiozc.async_unregister_service(self._dacp_info) - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - base_entries = await super().get_player_config_entries(player_id) - return (*base_entries, *PLAYER_CONFIG_ENTRIES) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player. - - - player_id: player_id of the player to handle the command. - """ - if airplay_player := self._players.get(player_id): - await airplay_player.cmd_stop() - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY (unpause) command to given player. - - - player_id: player_id of the player to handle the command. - """ - if airplay_player := self._players.get(player_id): - await airplay_player.cmd_play() - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player. - - - player_id: player_id of the player to handle the command. - """ - player = self.mass.players.get(player_id) - if player.group_childs: - # pause is not supported while synced, use stop instead - self.logger.debug("Player is synced, using STOP instead of PAUSE") - await self.cmd_stop(player_id) - return - airplay_player = self._players[player_id] - await airplay_player.cmd_pause() - - @lock - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - async with self._play_media_lock: - player = self.mass.players.get(player_id) - # set the active source for the player to the media queue - # this accounts for syncgroups and linked players (e.g. sonos) - player.active_source = media.queue_id - if player.synced_to: - # should not happen, but just in case - raise RuntimeError("Player is synced") - # always stop existing stream first - async with TaskManager(self.mass) as tg: - for airplay_player in self._get_sync_clients(player_id): - tg.create_task(airplay_player.cmd_stop(update_state=False)) - # select audio source - if media.media_type == MediaType.ANNOUNCEMENT: - # special case: stream announcement - input_format = AIRPLAY_PCM_FORMAT - audio_source = self.mass.streams.get_announcement_stream( - media.custom_data["url"], - output_format=AIRPLAY_PCM_FORMAT, - use_pre_announce=media.custom_data["use_pre_announce"], - ) - elif media.queue_id.startswith("ugp_"): - # special case: UGP stream - ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") - ugp_stream = ugp_provider.ugp_streams[media.queue_id] - input_format = ugp_stream.output_format - audio_source = ugp_stream.subscribe() - elif media.queue_id and media.queue_item_id: - # regular queue (flow) stream request - input_format = AIRPLAY_FLOW_PCM_FORMAT - audio_source = self.mass.streams.get_flow_stream( - queue=self.mass.player_queues.get(media.queue_id), - start_queue_item=self.mass.player_queues.get_item( - media.queue_id, media.queue_item_id - ), - pcm_format=input_format, - ) - else: - # assume url or some other direct path - # NOTE: this will fail if its an uri not playable by ffmpeg - input_format = AIRPLAY_PCM_FORMAT - audio_source = get_ffmpeg_stream( - audio_input=media.uri, - input_format=AudioFormat(ContentType.try_parse(media.uri)), - output_format=AIRPLAY_PCM_FORMAT, - ) - # setup RaopStreamSession for player (and its sync childs if any) - sync_clients = self._get_sync_clients(player_id) - raop_stream_session = RaopStreamSession(self, sync_clients, input_format, audio_source) - await raop_stream_session.start() - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player. - - - player_id: player_id of the player to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - airplay_player = self._players[player_id] - if airplay_player.raop_stream and airplay_player.raop_stream.running: - await airplay_player.raop_stream.send_cli_command(f"VOLUME={volume_level}\n") - mass_player = self.mass.players.get(player_id) - mass_player.volume_level = volume_level - mass_player.volume_muted = volume_level == 0 - self.mass.players.update(player_id) - # store last state in cache - await self.mass.cache.set(player_id, volume_level, base_key=CACHE_KEY_PREV_VOLUME) - - @lock - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - if player_id == target_player: - return - child_player = self.mass.players.get(player_id) - assert child_player # guard - parent_player = self.mass.players.get(target_player) - assert parent_player # guard - if parent_player.synced_to: - raise RuntimeError("Player is already synced") - if child_player.synced_to and child_player.synced_to != target_player: - raise RuntimeError("Player is already synced to another player") - if player_id in parent_player.group_childs: - # nothing to do: player is already part of the group - return - # ensure the child does not have an existing steam session active - if airplay_player := self._players.get(player_id): - if airplay_player.raop_stream and airplay_player.raop_stream.running: - await airplay_player.raop_stream.session.remove_client(airplay_player) - # always make sure that the parent player is part of the sync group - parent_player.group_childs.add(parent_player.player_id) - parent_player.group_childs.add(child_player.player_id) - child_player.synced_to = parent_player.player_id - # mark players as powered - parent_player.powered = True - child_player.powered = True - # check if we should (re)start or join a stream session - active_queue = self.mass.player_queues.get_active_queue(parent_player.player_id) - if active_queue.state == PlayerState.PLAYING: - # playback needs to be restarted to form a new multi client stream session - # this could potentially be called by multiple players at the exact same time - # so we debounce the resync a bit here with a timer - self.mass.call_later( - 1, - self.mass.player_queues.resume, - active_queue.queue_id, - fade_in=False, - task_id=f"resume_{active_queue.queue_id}", - ) - else: - # make sure that the player manager gets an update - self.mass.players.update(child_player.player_id, skip_forward=True) - self.mass.players.update(parent_player.player_id, skip_forward=True) - - @lock - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - mass_player = self.mass.players.get(player_id, raise_unavailable=True) - if not mass_player.synced_to: - return - ap_player = self._players[player_id] - if ap_player.raop_stream and ap_player.raop_stream.running: - await ap_player.raop_stream.session.remove_client(ap_player) - group_leader = self.mass.players.get(mass_player.synced_to, raise_unavailable=True) - if player_id in group_leader.group_childs: - group_leader.group_childs.remove(player_id) - mass_player.synced_to = None - airplay_player = self._players.get(player_id) - await airplay_player.cmd_stop() - # make sure that the player manager gets an update - self.mass.players.update(mass_player.player_id, skip_forward=True) - self.mass.players.update(group_leader.player_id, skip_forward=True) - - async def _getcliraop_binary(self): - """Find the correct raop/airplay binary belonging to the platform.""" - # ruff: noqa: SIM102 - if self.cliraop_bin is not None: - return self.cliraop_bin - - async def check_binary(cliraop_path: str) -> str | None: - try: - returncode, output = await check_output( - cliraop_path, - "-check", - ) - if returncode == 0 and output.strip().decode() == "cliraop check": - self.cliraop_bin = cliraop_path - return cliraop_path - except OSError: - return None - - base_path = os.path.join(os.path.dirname(__file__), "bin") - system = platform.system().lower().replace("darwin", "macos") - architecture = platform.machine().lower() - - if bridge_binary := await check_binary( - os.path.join(base_path, f"cliraop-{system}-{architecture}") - ): - return bridge_binary - - msg = f"Unable to locate RAOP Play binary for {system}/{architecture}" - raise RuntimeError(msg) - - def _get_sync_clients(self, player_id: str) -> list[AirPlayPlayer]: - """Get all sync clients for a player.""" - mass_player = self.mass.players.get(player_id, True) - sync_clients: list[AirPlayPlayer] = [] - # we need to return the player itself too - group_child_ids = {player_id} - group_child_ids.update(mass_player.group_childs) - for child_id in group_child_ids: - if client := self._players.get(child_id): - sync_clients.append(client) - return sync_clients - - async def _setup_player( - self, player_id: str, display_name: str, info: AsyncServiceInfo - ) -> None: - """Handle setup of a new player that is discovered using mdns.""" - address = get_primary_ip_address(info) - if address is None: - return - self.logger.debug("Discovered Airplay device %s on %s", display_name, address) - manufacturer, model = get_model_from_am(info.decoded_properties.get("am")) - if "apple tv" in model.lower(): - # For now, we ignore the Apple TV until we implement the authentication. - # maybe we can simply use pyatv only for this part? - # the cliraop application has already been prepared to accept the secret. - self.logger.debug( - "Ignoring %s in discovery due to authentication requirement.", display_name - ) - return - if not self.mass.config.get_raw_player_config_value(player_id, "enabled", True): - self.logger.debug("Ignoring %s in discovery as it is disabled.", display_name) - return - self._players[player_id] = AirPlayPlayer(self, player_id, info, address) - if not (volume := await self.mass.cache.get(player_id, base_key=CACHE_KEY_PREV_VOLUME)): - volume = FALLBACK_VOLUME - mass_player = Player( - player_id=player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=display_name, - available=True, - powered=False, - device_info=DeviceInfo( - model=model, - manufacturer=manufacturer, - address=address, - ), - supported_features=( - PlayerFeature.PAUSE, - PlayerFeature.SYNC, - PlayerFeature.VOLUME_SET, - ), - volume_level=volume, - ) - await self.mass.players.register_or_update(mass_player) - - async def _handle_dacp_request( # noqa: PLR0915 - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: - """Handle new connection on the socket.""" - try: - raw_request = b"" - while recv := await reader.read(1024): - raw_request += recv - if len(recv) < 1024: - break - - request = raw_request.decode("UTF-8") - if "\r\n\r\n" in request: - headers_raw, body = request.split("\r\n\r\n", 1) - else: - headers_raw = request - body = "" - headers_raw = headers_raw.split("\r\n") - headers = {} - for line in headers_raw[1:]: - if ":" not in line: - continue - x, y = line.split(":", 1) - headers[x.strip()] = y.strip() - active_remote = headers.get("Active-Remote") - _, path, _ = headers_raw[0].split(" ") - airplay_player = next( - ( - x - for x in self._players.values() - if x.raop_stream and x.raop_stream.active_remote_id == active_remote - ), - None, - ) - self.logger.debug( - "DACP request for %s (%s): %s -- %s", - airplay_player.discovery_info.name if airplay_player else "UNKNOWN PLAYER", - active_remote, - path, - body, - ) - if not airplay_player: - return - - player_id = airplay_player.player_id - mass_player = self.mass.players.get(player_id) - active_queue = self.mass.player_queues.get_active_queue(player_id) - if path == "/ctrl-int/1/nextitem": - self.mass.create_task(self.mass.player_queues.next(active_queue.queue_id)) - elif path == "/ctrl-int/1/previtem": - self.mass.create_task(self.mass.player_queues.previous(active_queue.queue_id)) - elif path == "/ctrl-int/1/play": - # sometimes this request is sent by a device as confirmation of a play command - # we ignore this if the player is already playing - if mass_player.state != PlayerState.PLAYING: - self.mass.create_task(self.mass.player_queues.play(active_queue.queue_id)) - elif path == "/ctrl-int/1/playpause": - self.mass.create_task(self.mass.player_queues.play_pause(active_queue.queue_id)) - elif path == "/ctrl-int/1/stop": - self.mass.create_task(self.mass.player_queues.stop(active_queue.queue_id)) - elif path == "/ctrl-int/1/volumeup": - self.mass.create_task(self.mass.players.cmd_volume_up(player_id)) - elif path == "/ctrl-int/1/volumedown": - self.mass.create_task(self.mass.players.cmd_volume_down(player_id)) - elif path == "/ctrl-int/1/shuffle_songs": - queue = self.mass.player_queues.get(player_id) - self.mass.loop.call_soon( - self.mass.player_queues.set_shuffle( - active_queue.queue_id, not queue.shuffle_enabled - ) - ) - elif path in ("/ctrl-int/1/pause", "/ctrl-int/1/discrete-pause"): - # sometimes this request is sent by a device as confirmation of a play command - # we ignore this if the player is already playing - if mass_player.state == PlayerState.PLAYING: - self.mass.create_task(self.mass.player_queues.pause(active_queue.queue_id)) - elif "dmcp.device-volume=" in path: - if mass_player.device_info.manufacturer.lower() == "apple": - # Apple devices only report their previous volume level ?! - return - # This is a bit annoying as this can be either the device confirming a new volume - # we've sent or the device requesting a new volume itself. - # In case of a small rounding difference, we ignore this, - # to prevent an endless pingpong of volume changes - raop_volume = float(path.split("dmcp.device-volume=", 1)[-1]) - volume = convert_airplay_volume(raop_volume) - if ( - abs(mass_player.volume_level - volume) > 5 - or (time.time() - airplay_player.last_command_sent) < 2 - ): - self.mass.create_task(self.cmd_volume_set(player_id, volume)) - else: - mass_player.volume_level = volume - self.mass.players.update(player_id) - elif "dmcp.volume=" in path: - # volume change request from device (e.g. volume buttons) - volume = int(path.split("dmcp.volume=", 1)[-1]) - if volume != mass_player.volume_level: - self.mass.create_task(self.cmd_volume_set(player_id, volume)) - # optimistically set the new volume to prevent bouncing around - mass_player.volume_level = volume - elif "device-prevent-playback=1" in path: - # device switched to another source (or is powered off) - if raop_stream := airplay_player.raop_stream: - # ignore this if we just started playing to prevent false positives - if mass_player.elapsed_time > 10 and mass_player.state == PlayerState.PLAYING: - raop_stream.prevent_playback = True - self.mass.create_task(self.monitor_prevent_playback(player_id)) - elif "device-prevent-playback=0" in path: - # device reports that its ready for playback again - if raop_stream := airplay_player.raop_stream: - raop_stream.prevent_playback = False - - # send response - date_str = utc().strftime("%a, %-d %b %Y %H:%M:%S") - response = ( - f"HTTP/1.0 204 No Content\r\nDate: {date_str} " - "GMT\r\nDAAP-Server: iTunes/7.6.2 (Windows; N;)\r\nContent-Type: " - "application/x-dmap-tagged\r\nContent-Length: 0\r\n" - "Connection: close\r\n\r\n" - ) - writer.write(response.encode()) - await writer.drain() - finally: - writer.close() - - async def monitor_prevent_playback(self, player_id: str): - """Monitor the prevent playback state of an airplay player.""" - count = 0 - if not (airplay_player := self._players.get(player_id)): - return - prev_active_remote_id = airplay_player.raop_stream.active_remote_id - while count < 40: - count += 1 - if not (airplay_player := self._players.get(player_id)): - return - if not (raop_stream := airplay_player.raop_stream): - return - if raop_stream.active_remote_id != prev_active_remote_id: - # checksum - return - if not raop_stream.prevent_playback: - return - await asyncio.sleep(0.5) - - airplay_player.logger.info( - "Player has been in prevent playback mode for too long, powering off.", - ) - await self.mass.players.cmd_power(airplay_player.player_id, False) diff --git a/music_assistant/server/providers/airplay/raop.py b/music_assistant/server/providers/airplay/raop.py deleted file mode 100644 index 27f4d701..00000000 --- a/music_assistant/server/providers/airplay/raop.py +++ /dev/null @@ -1,400 +0,0 @@ -"""Logic for RAOP (AirPlay 1) audio streaming to Airplay devices.""" - -from __future__ import annotations - -import asyncio -import logging -import os -import platform -import time -from collections.abc import AsyncGenerator -from contextlib import suppress -from random import randint -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import PlayerState -from music_assistant.constants import CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.audio import get_player_filter_params -from music_assistant.server.helpers.ffmpeg import FFMpeg -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.helpers.util import close_async_generator - -from .const import ( - AIRPLAY_PCM_FORMAT, - CONF_ALAC_ENCODE, - CONF_BIND_INTERFACE, - CONF_ENCRYPTION, - CONF_PASSWORD, - CONF_READ_AHEAD_BUFFER, -) - -if TYPE_CHECKING: - from music_assistant.common.models.media_items import AudioFormat - from music_assistant.common.models.player_queue import PlayerQueue - - from .player import AirPlayPlayer - from .provider import AirplayProvider - - -class RaopStreamSession: - """Object that holds the details of a (RAOP) stream session to one or more players.""" - - def __init__( - self, - airplay_provider: AirplayProvider, - sync_clients: list[AirPlayPlayer], - input_format: AudioFormat, - audio_source: AsyncGenerator[bytes, None], - ) -> None: - """Initialize RaopStreamSession.""" - assert sync_clients - self.prov = airplay_provider - self.mass = airplay_provider.mass - self.input_format = input_format - self._sync_clients = sync_clients - self._audio_source = audio_source - self._audio_source_task: asyncio.Task | None = None - self._stopped: bool = False - self._lock = asyncio.Lock() - - async def start(self) -> None: - """Initialize RaopStreamSession.""" - # initialize raop stream for all players - for airplay_player in self._sync_clients: - if airplay_player.raop_stream and airplay_player.raop_stream.running: - raise RuntimeError("Player already has an active stream") - airplay_player.raop_stream = RaopStream(self, airplay_player) - - async def audio_streamer() -> None: - """Stream audio to all players.""" - generator_exhausted = False - try: - async for chunk in self._audio_source: - if not self._sync_clients: - return - async with self._lock: - await asyncio.gather( - *[x.raop_stream.write_chunk(chunk) for x in self._sync_clients], - return_exceptions=True, - ) - # entire stream consumed: send EOF - generator_exhausted = True - async with self._lock: - await asyncio.gather( - *[x.raop_stream.write_eof() for x in self._sync_clients], - return_exceptions=True, - ) - finally: - if not generator_exhausted: - await close_async_generator(self._audio_source) - - # get current ntp and start RaopStream per player - _, stdout = await check_output(self.prov.cliraop_bin, "-ntp") - start_ntp = int(stdout.strip()) - wait_start = 1500 + (250 * len(self._sync_clients)) - async with self._lock: - await asyncio.gather( - *[x.raop_stream.start(start_ntp, wait_start) for x in self._sync_clients], - return_exceptions=True, - ) - self._audio_source_task = asyncio.create_task(audio_streamer()) - - async def stop(self) -> None: - """Stop playback and cleanup.""" - if self._stopped: - return - self._stopped = True - if self._audio_source_task and not self._audio_source_task.done(): - self._audio_source_task.cancel() - await asyncio.gather( - *[self.remove_client(x) for x in self._sync_clients], - return_exceptions=True, - ) - - async def remove_client(self, airplay_player: AirPlayPlayer) -> None: - """Remove a sync client from the session.""" - if airplay_player not in self._sync_clients: - return - assert airplay_player.raop_stream.session == self - async with self._lock: - self._sync_clients.remove(airplay_player) - await airplay_player.raop_stream.stop() - airplay_player.raop_stream = None - - async def add_client(self, airplay_player: AirPlayPlayer) -> None: - """Add a sync client to the session.""" - # TODO: Add the ability to add a new client to an existing session - # e.g. by counting the number of frames sent etc. - raise NotImplementedError("Adding clients to a session is not yet supported") - - -class RaopStream: - """ - RAOP (Airplay 1) Audio Streamer. - - Python is not suitable for realtime audio streaming so we do the actual streaming - of (RAOP) audio using a small executable written in C based on libraop to do - the actual timestamped playback, which reads pcm audio from stdin - and we can send some interactive commands using a named pipe. - """ - - def __init__( - self, - session: RaopStreamSession, - airplay_player: AirPlayPlayer, - ) -> None: - """Initialize RaopStream.""" - self.session = session - self.prov = session.prov - self.mass = session.prov.mass - self.airplay_player = airplay_player - - # always generate a new active remote id to prevent race conditions - # with the named pipe used to send audio - self.active_remote_id: str = str(randint(1000, 8000)) - self.prevent_playback: bool = False - self._log_reader_task: asyncio.Task | None = None - self._cliraop_proc: AsyncProcess | None = None - self._ffmpeg_proc: AsyncProcess | None = None - self._started = asyncio.Event() - self._stopped = False - - @property - def running(self) -> bool: - """Return boolean if this stream is running.""" - return not self._stopped and self._started.is_set() - - async def start(self, start_ntp: int, wait_start: int = 1000) -> None: - """Initialize CLIRaop process for a player.""" - extra_args = [] - player_id = self.airplay_player.player_id - mass_player = self.mass.players.get(player_id) - bind_ip = await self.mass.config.get_provider_config_value( - self.prov.instance_id, CONF_BIND_INTERFACE - ) - extra_args += ["-if", bind_ip] - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENCRYPTION, False): - extra_args += ["-encrypt"] - if self.mass.config.get_raw_player_config_value(player_id, CONF_ALAC_ENCODE, True): - extra_args += ["-alac"] - for prop in ("et", "md", "am", "pk", "pw"): - if prop_value := self.airplay_player.discovery_info.decoded_properties.get(prop): - extra_args += [f"-{prop}", prop_value] - sync_adjust = self.mass.config.get_raw_player_config_value(player_id, CONF_SYNC_ADJUST, 0) - if device_password := self.mass.config.get_raw_player_config_value( - player_id, CONF_PASSWORD, None - ): - extra_args += ["-password", device_password] - if self.prov.logger.isEnabledFor(logging.DEBUG): - extra_args += ["-debug", "5"] - elif self.prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - extra_args += ["-debug", "10"] - read_ahead = await self.mass.config.get_player_config_value( - player_id, CONF_READ_AHEAD_BUFFER - ) - - # create os pipes to pipe ffmpeg to cliraop - read, write = await asyncio.to_thread(os.pipe) - - # ffmpeg handles the player specific stream + filters and pipes - # audio to the cliraop process - self._ffmpeg_proc = FFMpeg( - audio_input="-", - input_format=self.session.input_format, - output_format=AIRPLAY_PCM_FORMAT, - filter_params=get_player_filter_params(self.mass, player_id), - audio_output=write, - ) - await self._ffmpeg_proc.start() - await asyncio.to_thread(os.close, write) - - # cliraop is the binary that handles the actual raop streaming to the player - cliraop_args = [ - self.prov.cliraop_bin, - "-ntpstart", - str(start_ntp), - "-port", - str(self.airplay_player.discovery_info.port), - "-wait", - str(wait_start - sync_adjust), - "-latency", - str(read_ahead), - "-volume", - str(mass_player.volume_level), - *extra_args, - "-dacp", - self.prov.dacp_id, - "-activeremote", - self.active_remote_id, - "-udn", - self.airplay_player.discovery_info.name, - self.airplay_player.address, - "-", - ] - self._cliraop_proc = AsyncProcess(cliraop_args, stdin=read, stderr=True, name="cliraop") - if platform.system() == "Darwin": - os.environ["DYLD_LIBRARY_PATH"] = "/usr/local/lib" - await self._cliraop_proc.start() - await asyncio.to_thread(os.close, read) - self._started.set() - self._log_reader_task = self.mass.create_task(self._log_watcher()) - - async def stop(self): - """Stop playback and cleanup.""" - if self._stopped: - return - if self._cliraop_proc.proc and not self._cliraop_proc.closed: - await self.send_cli_command("ACTION=STOP") - self._stopped = True # set after send_cli command! - if self._cliraop_proc.proc and not self._cliraop_proc.closed: - await self._cliraop_proc.close(True) - if self._ffmpeg_proc and not self._ffmpeg_proc.closed: - await self._ffmpeg_proc.close(True) - self._cliraop_proc = None - self._ffmpeg_proc = None - - async def write_chunk(self, chunk: bytes) -> None: - """Write a (pcm) audio chunk.""" - if self._stopped: - return - await self._started.wait() - await self._ffmpeg_proc.write(chunk) - - async def write_eof(self) -> None: - """Write EOF.""" - if self._stopped: - return - await self._started.wait() - await self._ffmpeg_proc.write_eof() - - async def send_cli_command(self, command: str) -> None: - """Send an interactive command to the running CLIRaop binary.""" - if self._stopped: - return - await self._started.wait() - - if not command.endswith("\n"): - command += "\n" - - def send_data(): - with suppress(BrokenPipeError), open(named_pipe, "w") as f: - f.write(command) - - named_pipe = f"/tmp/raop-{self.active_remote_id}" # noqa: S108 - self.airplay_player.logger.log(VERBOSE_LOG_LEVEL, "sending command %s", command) - self.airplay_player.last_command_sent = time.time() - await asyncio.to_thread(send_data) - - async def _log_watcher(self) -> None: - """Monitor stderr for the running CLIRaop process.""" - airplay_player = self.airplay_player - mass_player = self.mass.players.get(airplay_player.player_id) - queue = self.mass.player_queues.get_active_queue(mass_player.active_source) - logger = airplay_player.logger - lost_packets = 0 - prev_metadata_checksum: str = "" - prev_progress_report: float = 0 - async for line in self._cliraop_proc.iter_stderr(): - if "elapsed milliseconds:" in line: - # this is received more or less every second while playing - millis = int(line.split("elapsed milliseconds: ")[1]) - mass_player.elapsed_time = millis / 1000 - mass_player.elapsed_time_last_updated = time.time() - # send metadata to player(s) if needed - # NOTE: this must all be done in separate tasks to not disturb audio - now = time.time() - if ( - mass_player.elapsed_time > 2 - and queue - and queue.current_item - and queue.current_item.streamdetails - ): - metadata_checksum = ( - queue.current_item.streamdetails.stream_title - or queue.current_item.queue_item_id - ) - if prev_metadata_checksum != metadata_checksum: - prev_metadata_checksum = metadata_checksum - prev_progress_report = now - self.mass.create_task(self._send_metadata(queue)) - # send the progress report every 5 seconds - elif now - prev_progress_report >= 5: - prev_progress_report = now - self.mass.create_task(self._send_progress(queue)) - if "set pause" in line or "Pause at" in line: - mass_player.state = PlayerState.PAUSED - self.mass.players.update(airplay_player.player_id) - if "Restarted at" in line or "restarting w/ pause" in line: - mass_player.state = PlayerState.PLAYING - self.mass.players.update(airplay_player.player_id) - if "restarting w/o pause" in line: - # streaming has started - mass_player.state = PlayerState.PLAYING - mass_player.elapsed_time = 0 - mass_player.elapsed_time_last_updated = time.time() - self.mass.players.update(airplay_player.player_id) - if "lost packet out of backlog" in line: - 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)) - else: - logger.warning("Packet loss detected!") - if "end of stream reached" in line: - logger.debug("End of stream reached") - break - - logger.log(VERBOSE_LOG_LEVEL, line) - - # if we reach this point, the process exited - if airplay_player.raop_stream == self: - mass_player.state = PlayerState.IDLE - self.mass.players.update(airplay_player.player_id) - # ensure we're cleaned up afterwards (this also logs the returncode) - await self.stop() - - async def _send_metadata(self, queue: PlayerQueue) -> None: - """Send metadata to player (and connected sync childs).""" - if not queue or not queue.current_item: - return - duration = min(queue.current_item.duration or 0, 3600) - title = queue.current_item.name - artist = "" - album = "" - if queue.current_item.streamdetails and queue.current_item.streamdetails.stream_title: - # stream title from radio station - stream_title = queue.current_item.streamdetails.stream_title - if " - " in stream_title: - artist, title = stream_title.split(" - ", 1) - else: - title = stream_title - # set album to radio station name - album = queue.current_item.name - elif media_item := queue.current_item.media_item: - title = media_item.name - if artist_str := getattr(media_item, "artist_str", None): - artist = artist_str - if _album := getattr(media_item, "album", None): - album = _album.name - - cmd = f"TITLE={title or 'Music Assistant'}\nARTIST={artist}\nALBUM={album}\n" - cmd += f"DURATION={duration}\nPROGRESS=0\nACTION=SENDMETA\n" - - await self.send_cli_command(cmd) - - # get image - if not queue.current_item.image: - return - - # the image format needs to be 500x500 jpeg for maximum compatibility with players - image_url = self.mass.metadata.get_image_url( - queue.current_item.image, size=500, prefer_proxy=True, image_format="jpeg" - ) - await self.send_cli_command(f"ARTWORK={image_url}\n") - - async def _send_progress(self, queue: PlayerQueue) -> None: - """Send progress report to player (and connected sync childs).""" - if not queue or not queue.current_item: - return - progress = int(queue.corrected_elapsed_time) - await self.send_cli_command(f"PROGRESS={progress}\n") diff --git a/music_assistant/server/providers/apple_music/__init__.py b/music_assistant/server/providers/apple_music/__init__.py deleted file mode 100644 index 03d69a54..00000000 --- a/music_assistant/server/providers/apple_music/__init__.py +++ /dev/null @@ -1,814 +0,0 @@ -"""Apple Music musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -import base64 -import json -import os -from typing import TYPE_CHECKING, Any - -import aiofiles -from pywidevine import PSSH, Cdm, Device, DeviceTypes -from pywidevine.license_protocol_pb2 import WidevinePsshData - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - AudioFormat, - ContentType, - ImageType, - ItemMapping, - MediaItemImage, - MediaItemType, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import CONF_PASSWORD -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.playlists import fetch_playlist -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, -) - -DEVELOPER_TOKEN = app_var(8) -WIDEVINE_BASE_PATH = "/usr/local/bin/widevine_cdm" -DECRYPT_CLIENT_ID_FILENAME = "client_id.bin" -DECRYPT_PRIVATE_KEY_FILENAME = "private_key.pem" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return AppleMusicProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Music user token", - required=True, - ), - ) - - -class AppleMusicProvider(MusicProvider): - """Implementation of an Apple Music MusicProvider.""" - - _music_user_token: str | None = None - _storefront: str | None = None - _decrypt_client_id: bytes | None = None - _decrypt_private_key: bytes | None = None - # rate limiter needs to be specified on provider-level, - # so make it an instance attribute - throttler = ThrottlerManager(rate_limit=1, period=2, initial_backoff=15) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self._music_user_token = self.config.get_value(CONF_PASSWORD) - self._storefront = await self._get_user_storefront() - async with aiofiles.open( - os.path.join(WIDEVINE_BASE_PATH, DECRYPT_CLIENT_ID_FILENAME), "rb" - ) as _file: - self._decrypt_client_id = await _file.read() - async with aiofiles.open( - os.path.join(WIDEVINE_BASE_PATH, DECRYPT_PRIVATE_KEY_FILENAME), "rb" - ) as _file: - self._decrypt_private_key = await _file.read() - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - endpoint = f"catalog/{self._storefront}/search" - # Apple music has a limit of 25 items for the search endpoint - limit = min(limit, 25) - searchresult = SearchResults() - searchtypes = [] - if MediaType.ARTIST in media_types: - searchtypes.append("artists") - if MediaType.ALBUM in media_types: - searchtypes.append("albums") - if MediaType.TRACK in media_types: - searchtypes.append("songs") - if MediaType.PLAYLIST in media_types: - searchtypes.append("playlists") - if not searchtypes: - return searchresult - searchtype = ",".join(searchtypes) - search_query = search_query.replace("'", "") - response = await self._get_data(endpoint, term=search_query, types=searchtype, limit=limit) - if "artists" in response["results"]: - searchresult.artists += [ - self._parse_artist(item) for item in response["results"]["artists"]["data"] - ] - if "albums" in response["results"]: - searchresult.albums += [ - self._parse_album(item) for item in response["results"]["albums"]["data"] - ] - if "songs" in response["results"]: - searchresult.tracks += [ - self._parse_track(item) for item in response["results"]["songs"]["data"] - ] - if "playlists" in response["results"]: - searchresult.playlists += [ - self._parse_playlist(item) for item in response["results"]["playlists"]["data"] - ] - return searchresult - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve library artists from spotify.""" - endpoint = "me/library/artists" - for item in await self._get_all_items(endpoint, include="catalog", extend="editorialNotes"): - if item and item["id"]: - yield self._parse_artist(item) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve library albums from the provider.""" - endpoint = "me/library/albums" - for item in await self._get_all_items( - endpoint, include="catalog,artists", extend="editorialNotes" - ): - if item and item["id"]: - album = self._parse_album(item) - if album: - yield album - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - endpoint = "me/library/songs" - song_catalog_ids = [] - for item in await self._get_all_items(endpoint): - catalog_id = item.get("attributes", {}).get("playParams", {}).get("catalogId") - if not catalog_id: - self.logger.debug( - "Skipping track. No catalog version found for %s - %s", - item["attributes"].get("artistName", ""), - item["attributes"].get("name", ""), - ) - continue - song_catalog_ids.append(catalog_id) - # Obtain catalog info per 200 songs, the documented limit of 300 results in a 504 timeout - max_limit = 200 - for i in range(0, len(song_catalog_ids), max_limit): - catalog_ids = song_catalog_ids[i : i + max_limit] - catalog_endpoint = f"catalog/{self._storefront}/songs" - response = await self._get_data( - catalog_endpoint, ids=",".join(catalog_ids), include="artists,albums" - ) - for item in response["data"]: - yield self._parse_track(item) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve playlists from the provider.""" - endpoint = "me/library/playlists" - for item in await self._get_all_items(endpoint): - # Prefer catalog information over library information in case of public playlists - if item["attributes"]["hasCatalog"]: - yield await self.get_playlist(item["attributes"]["playParams"]["globalId"]) - elif item and item["id"]: - yield self._parse_playlist(item) - - async def get_artist(self, prov_artist_id) -> Artist: - """Get full artist details by id.""" - endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}" - response = await self._get_data(endpoint, extend="editorialNotes") - return self._parse_artist(response["data"][0]) - - async def get_album(self, prov_album_id) -> Album: - """Get full album details by id.""" - endpoint = f"catalog/{self._storefront}/albums/{prov_album_id}" - response = await self._get_data(endpoint, include="artists") - return self._parse_album(response["data"][0]) - - async def get_track(self, prov_track_id) -> Track: - """Get full track details by id.""" - endpoint = f"catalog/{self._storefront}/songs/{prov_track_id}" - response = await self._get_data(endpoint, include="artists,albums") - return self._parse_track(response["data"][0]) - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Get full playlist details by id.""" - if self._is_catalog_id(prov_playlist_id): - endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}" - else: - endpoint = f"me/library/playlists/{prov_playlist_id}" - endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}" - response = await self._get_data(endpoint) - return self._parse_playlist(response["data"][0]) - - async def get_album_tracks(self, prov_album_id) -> list[Track]: - """Get all album tracks for given album id.""" - endpoint = f"catalog/{self._storefront}/albums/{prov_album_id}/tracks" - response = await self._get_data(endpoint, include="artists") - # Including albums results in a 504 error, so we need to fetch the album separately - album = await self.get_album(prov_album_id) - tracks = [] - for track_obj in response["data"]: - if "id" not in track_obj: - continue - track = self._parse_track(track_obj) - track.album = album - tracks.append(track) - return tracks - - async def get_playlist_tracks(self, prov_playlist_id, page: int = 0) -> list[Track]: - """Get all playlist tracks for given playlist id.""" - if self._is_catalog_id(prov_playlist_id): - endpoint = f"catalog/{self._storefront}/playlists/{prov_playlist_id}/tracks" - else: - endpoint = f"me/library/playlists/{prov_playlist_id}/tracks" - result = [] - page_size = 100 - offset = page * page_size - response = await self._get_data( - endpoint, include="artists,catalog", limit=page_size, offset=offset - ) - if not response or "data" not in response: - return result - for index, track in enumerate(response["data"]): - if track and track["id"]: - parsed_track = self._parse_track(track) - parsed_track.position = offset + index + 1 - result.append(parsed_track) - return result - - async def get_artist_albums(self, prov_artist_id) -> list[Album]: - """Get a list of all albums for the given artist.""" - endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}/albums" - try: - response = await self._get_all_items(endpoint) - except MediaNotFoundError: - # Some artists do not have albums, return empty list - self.logger.info("No albums found for artist %s", prov_artist_id) - return [] - return [self._parse_album(album) for album in response if album["id"]] - - async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: - """Get a list of 10 most popular tracks for the given artist.""" - endpoint = f"catalog/{self._storefront}/artists/{prov_artist_id}/view/top-songs" - try: - response = await self._get_data(endpoint) - except MediaNotFoundError: - # Some artists do not have top tracks, return empty list - self.logger.info("No top tracks found for artist %s", prov_artist_id) - return [] - return [self._parse_track(track) for track in response["data"] if track["id"]] - - async def library_add(self, item: MediaItemType): - """Add item to library.""" - raise NotImplementedError("Not implemented!") - - async def library_remove(self, prov_item_id, media_type: MediaType): - """Remove item from library.""" - raise NotImplementedError("Not implemented!") - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]): - """Add track(s) to playlist.""" - raise NotImplementedError("Not implemented!") - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - raise NotImplementedError("Not implemented!") - - async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - # Note, Apple music does not have an official endpoint for similar tracks. - # We will use the next-tracks endpoint to get a list of tracks that are similar to the - # provided track. However, Apple music only provides 2 tracks at a time, so we will - # need to call the endpoint multiple times. Therefore, set a limit to 6 to prevent - # flooding the apple music api. - limit = 6 - endpoint = f"me/stations/next-tracks/ra.{prov_track_id}" - found_tracks = [] - while len(found_tracks) < limit: - response = await self._post_data(endpoint, include="artists") - if not response or "data" not in response: - break - for track in response["data"]: - if track and track["id"]: - found_tracks.append(self._parse_track(track)) - return found_tracks - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - stream_metadata = await self._fetch_song_stream_metadata(item_id) - license_url = stream_metadata["hls-key-server-url"] - stream_url, uri = await self._parse_stream_url_and_uri(stream_metadata["assets"]) - if not stream_url or not uri: - raise MediaNotFoundError("No stream URL found for song.") - key_id = base64.b64decode(uri.split(",")[1]) - return StreamDetails( - item_id=item_id, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.UNKNOWN, - ), - stream_type=StreamType.ENCRYPTED_HTTP, - path=stream_url, - decryption_key=await self._get_decryption_key(license_url, key_id, uri, item_id), - can_seek=True, - ) - - def _parse_artist(self, artist_obj): - """Parse artist object to generic layout.""" - relationships = artist_obj.get("relationships", {}) - if ( - artist_obj.get("type") == "library-artists" - and relationships.get("catalog", {}).get("data", []) != [] - ): - artist_id = relationships["catalog"]["data"][0]["id"] - attributes = relationships["catalog"]["data"][0]["attributes"] - elif "attributes" in artist_obj: - artist_id = artist_obj["id"] - attributes = artist_obj["attributes"] - else: - artist_id = artist_obj["id"] - self.logger.debug("No attributes found for artist %s", artist_obj) - # No more details available other than the id, return an ItemMapping - return ItemMapping( - media_type=MediaType.ARTIST, - provider=self.instance_id, - item_id=artist_id, - name=artist_id, - ) - artist = Artist( - item_id=artist_id, - name=attributes.get("name"), - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=artist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=attributes.get("url"), - ) - }, - ) - if artwork := attributes.get("artwork"): - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if genres := attributes.get("genreNames"): - artist.metadata.genres = set(genres) - if notes := attributes.get("editorialNotes"): - artist.metadata.description = notes.get("standard") or notes.get("short") - return artist - - def _parse_album(self, album_obj: dict) -> Album | ItemMapping | None: - """Parse album object to generic layout.""" - relationships = album_obj.get("relationships", {}) - response_type = album_obj.get("type") - if ( - response_type == "library-albums" - and relationships["catalog"]["data"] != [] - and "attributes" in relationships["catalog"]["data"][0] - ): - album_id = relationships.get("catalog", {})["data"][0]["id"] - attributes = relationships.get("catalog", {})["data"][0]["attributes"] - elif "attributes" in album_obj: - album_id = album_obj["id"] - attributes = album_obj["attributes"] - else: - album_id = album_obj["id"] - # No more details available other than the id, return an ItemMapping - return ItemMapping( - media_type=MediaType.ALBUM, - provider=self.instance_id, - item_id=album_id, - name=album_id, - ) - is_available_in_catalog = attributes.get("url") is not None - if not is_available_in_catalog: - self.logger.debug( - "Skipping album %s. Album is not available in the Apple Music catalog.", - attributes.get("name"), - ) - return None - album = Album( - item_id=album_id, - provider=self.domain, - name=attributes.get("name"), - provider_mappings={ - ProviderMapping( - item_id=album_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=attributes.get("url"), - available=attributes.get("playParams", {}).get("id") is not None, - ) - }, - ) - if artists := relationships.get("artists"): - album.artists = [self._parse_artist(artist) for artist in artists["data"]] - elif artist_name := attributes.get("artistName"): - album.artists = [ - ItemMapping( - media_type=MediaType.ARTIST, - provider=self.instance_id, - item_id=artist_name, - name=artist_name, - ) - ] - if release_date := attributes.get("releaseDate"): - album.year = int(release_date.split("-")[0]) - if genres := attributes.get("genreNames"): - album.metadata.genres = set(genres) - if artwork := attributes.get("artwork"): - album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if album_copyright := attributes.get("copyright"): - album.metadata.copyright = album_copyright - if record_label := attributes.get("recordLabel"): - album.metadata.label = record_label - if upc := attributes.get("upc"): - album.external_ids.add((ExternalID.BARCODE, "0" + upc)) - if notes := attributes.get("editorialNotes"): - album.metadata.description = notes.get("standard") or notes.get("short") - if content_rating := attributes.get("contentRating"): - album.metadata.explicit = content_rating == "explicit" - album_type = AlbumType.ALBUM - if attributes.get("isSingle"): - album_type = AlbumType.SINGLE - elif attributes.get("isCompilation"): - album_type = AlbumType.COMPILATION - album.album_type = album_type - return album - - def _parse_track( - self, - track_obj: dict[str, Any], - ) -> Track: - """Parse track object to generic layout.""" - relationships = track_obj.get("relationships", {}) - if track_obj.get("type") == "library-songs" and relationships["catalog"]["data"] != []: - track_id = relationships.get("catalog", {})["data"][0]["id"] - attributes = relationships.get("catalog", {})["data"][0]["attributes"] - elif "attributes" in track_obj: - track_id = track_obj["id"] - attributes = track_obj["attributes"] - else: - track_id = track_obj["id"] - attributes = {} - track = Track( - item_id=track_id, - provider=self.domain, - name=attributes.get("name"), - duration=attributes.get("durationInMillis", 0) / 1000, - provider_mappings={ - ProviderMapping( - item_id=track_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat(content_type=ContentType.AAC), - url=attributes.get("url"), - available=attributes.get("playParams", {}).get("id") is not None, - ) - }, - ) - if disc_number := attributes.get("discNumber"): - track.disc_number = disc_number - if track_number := attributes.get("trackNumber"): - track.track_number = track_number - # Prefer catalog information over library information for artists. - # For compilations it picks the wrong artists - if "artists" in relationships: - artists = relationships["artists"] - track.artists = [self._parse_artist(artist) for artist in artists["data"]] - # 'Similar tracks' do not provide full artist details - elif artist_name := attributes.get("artistName"): - track.artists = [ - ItemMapping( - media_type=MediaType.ARTIST, - item_id=artist_name, - provider=self.instance_id, - name=artist_name, - ) - ] - if albums := relationships.get("albums"): - if "data" in albums and len(albums["data"]) > 0: - track.album = self._parse_album(albums["data"][0]) - if artwork := attributes.get("artwork"): - track.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=artwork["url"].format(w=artwork["width"], h=artwork["height"]), - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if genres := attributes.get("genreNames"): - track.metadata.genres = set(genres) - if composers := attributes.get("composerName"): - track.metadata.performers = set(composers.split(", ")) - if isrc := attributes.get("isrc"): - track.external_ids.add((ExternalID.ISRC, isrc)) - return track - - def _parse_playlist(self, playlist_obj) -> Playlist: - """Parse Apple Music playlist object to generic layout.""" - attributes = playlist_obj["attributes"] - playlist_id = attributes["playParams"].get("globalId") or playlist_obj["id"] - playlist = Playlist( - item_id=playlist_id, - provider=self.domain, - name=attributes["name"], - owner=attributes.get("curatorName", "me"), - provider_mappings={ - ProviderMapping( - item_id=playlist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=attributes.get("url"), - ) - }, - ) - if artwork := attributes.get("artwork"): - url = artwork["url"] - if artwork["width"] and artwork["height"]: - url = url.format(w=artwork["width"], h=artwork["height"]) - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if description := attributes.get("description"): - playlist.metadata.description = description.get("standard") - playlist.is_editable = attributes.get("canEdit", False) - if checksum := attributes.get("lastModifiedDate"): - playlist.cache_checksum = checksum - return playlist - - async def _get_all_items(self, endpoint, key="data", **kwargs) -> list[dict]: - """Get all items from a paged list.""" - limit = 50 - offset = 0 - all_items = [] - while True: - kwargs["limit"] = limit - kwargs["offset"] = offset - result = await self._get_data(endpoint, **kwargs) - if key not in result: - break - all_items += result[key] - if not result.get("next"): - break - offset += limit - return all_items - - @throttle_with_retries - async def _get_data(self, endpoint, **kwargs) -> dict[str, Any]: - """Get data from api.""" - url = f"https://api.music.apple.com/v1/{endpoint}" - headers = {"Authorization": f"Bearer {DEVELOPER_TOKEN}"} - headers["Music-User-Token"] = self._music_user_token - async with ( - self.mass.http_session.get( - url, headers=headers, params=kwargs, ssl=True, timeout=120 - ) as response, - ): - if response.status == 404 and "limit" in kwargs and "offset" in kwargs: - return {} - # Convert HTTP errors to exceptions - if response.status == 404: - raise MediaNotFoundError(f"{endpoint} not found") - if response.status == 504: - # See if we can get more info from the response on occasional timeouts - self.logger.debug( - "Apple Music API Timeout: url=%s, params=%s, response_headers=%s", - url, - kwargs, - response.headers, - ) - raise ResourceTemporarilyUnavailable("Apple Music API Timeout") - if response.status == 429: - # Debug this for now to see if the response headers give us info about the - # backoff time. There is no documentation on this. - self.logger.debug("Apple Music Rate Limiter. Headers: %s", response.headers) - raise ResourceTemporarilyUnavailable("Apple Music Rate Limiter") - response.raise_for_status() - return await response.json(loads=json_loads) - - async def _delete_data(self, endpoint, data=None, **kwargs) -> str: - """Delete data from api.""" - raise NotImplementedError("Not implemented!") - - async def _put_data(self, endpoint, data=None, **kwargs) -> str: - """Put data on api.""" - raise NotImplementedError("Not implemented!") - - @throttle_with_retries - async def _post_data(self, endpoint, data=None, **kwargs) -> str: - """Post data on api.""" - url = f"https://api.music.apple.com/v1/{endpoint}" - headers = {"Authorization": f"Bearer {DEVELOPER_TOKEN}"} - headers["Music-User-Token"] = self._music_user_token - async with ( - self.mass.http_session.post( - url, headers=headers, params=kwargs, json=data, ssl=True, timeout=120 - ) as response, - ): - # Convert HTTP errors to exceptions - if response.status == 404: - raise MediaNotFoundError(f"{endpoint} not found") - if response.status == 429: - # Debug this for now to see if the response headers give us info about the - # backoff time. There is no documentation on this. - self.logger.debug("Apple Music Rate Limiter. Headers: %s", response.headers) - raise ResourceTemporarilyUnavailable("Apple Music Rate Limiter") - response.raise_for_status() - return await response.json(loads=json_loads) - - async def _get_user_storefront(self) -> str: - """Get the user's storefront.""" - locale = self.mass.metadata.locale.replace("_", "-") - language = locale.split("-")[0] - result = await self._get_data("me/storefront", l=language) - return result["data"][0]["id"] - - def _is_catalog_id(self, catalog_id: str) -> bool: - """Check if input is a catalog id, or a library id.""" - return catalog_id.isnumeric() or catalog_id.startswith("pl.") - - async def _fetch_song_stream_metadata(self, song_id: str) -> str: - """Get the stream URL for a song from Apple Music.""" - playback_url = "https://play.music.apple.com/WebObjects/MZPlay.woa/wa/webPlayback" - data = { - "salableAdamId": song_id, - } - async with self.mass.http_session.post( - playback_url, headers=self._get_decryption_headers(), json=data, ssl=True - ) as response: - response.raise_for_status() - content = await response.json(loads=json_loads) - return content["songList"][0] - - async def _parse_stream_url_and_uri(self, stream_assets: list[dict]) -> str: - """Parse the Stream URL and Key URI from the song.""" - ctrp256_urls = [asset["URL"] for asset in stream_assets if asset["flavor"] == "28:ctrp256"] - if len(ctrp256_urls) == 0: - raise MediaNotFoundError("No ctrp256 URL found for song.") - playlist_url = ctrp256_urls[0] - playlist_items = await fetch_playlist(self.mass, ctrp256_urls[0], raise_on_hls=False) - # Apple returns a HLS (substream) playlist but instead of chunks, - # each item is just the whole file. So we simply grab the first playlist item. - playlist_item = playlist_items[0] - # path is relative, stitch it together - base_path = playlist_url.rsplit("/", 1)[0] - track_url = base_path + "/" + playlist_items[0].path - key = playlist_item.key - return (track_url, key) - - def _get_decryption_headers(self): - """Get headers for decryption requests.""" - return { - "authorization": f"Bearer {DEVELOPER_TOKEN}", - "media-user-token": self._music_user_token, - "connection": "keep-alive", - "accept": "application/json", - "origin": "https://music.apple.com", - "referer": "https://music.apple.com/", - "accept-encoding": "gzip, deflate, br", - "content-type": "application/json;charset=utf-8", - "user-agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" - " Chrome/110.0.0.0 Safari/537.36" - ), - } - - async def _get_decryption_key( - self, license_url: str, key_id: str, uri: str, item_id: str - ) -> str: - """Get the decryption key for a song.""" - cache_key = f"decryption_key.{item_id}" - if decryption_key := await self.mass.cache.get(cache_key, base_key=self.instance_id): - self.logger.debug("Decryption key for %s found in cache.", item_id) - return decryption_key - pssh = self._get_pssh(key_id) - device = Device( - client_id=self._decrypt_client_id, - private_key=self._decrypt_private_key, - type_=DeviceTypes.ANDROID, - security_level=3, - flags={}, - ) - cdm = Cdm.from_device(device) - session_id = cdm.open() - challenge = cdm.get_license_challenge(session_id, pssh) - track_license = await self._get_license(challenge, license_url, uri, item_id) - cdm.parse_license(session_id, track_license) - key = next(key for key in cdm.get_keys(session_id) if key.type == "CONTENT") - if not key: - raise MediaNotFoundError("Unable to get decryption key for song %s.", item_id) - cdm.close(session_id) - decryption_key = key.key.hex() - self.mass.create_task( - self.mass.cache.set( - cache_key, decryption_key, expiration=7200, base_key=self.instance_id - ) - ) - return decryption_key - - def _get_pssh(self, key_id: bytes) -> PSSH: - """Get the PSSH for a song.""" - pssh_data = WidevinePsshData() - pssh_data.algorithm = 1 - pssh_data.key_ids.append(key_id) - init_data = base64.b64encode(pssh_data.SerializeToString()).decode("utf-8") - return PSSH.new(system_id=PSSH.SystemId.Widevine, init_data=init_data) - - async def _get_license(self, challenge: bytes, license_url: str, uri: str, item_id: str) -> str: - """Get the license for a song based on the challenge.""" - challenge_b64 = base64.b64encode(challenge).decode("utf-8") - data = { - "challenge": challenge_b64, - "key-system": "com.widevine.alpha", - "uri": uri, - "adamId": item_id, - "isLibrary": False, - "user-initiated": True, - } - async with self.mass.http_session.post( - license_url, data=json.dumps(data), headers=self._get_decryption_headers(), ssl=False - ) as response: - response.raise_for_status() - content = await response.json(loads=json_loads) - track_license = content.get("license") - if not track_license: - raise MediaNotFoundError("No license found for song %s.", item_id) - return track_license diff --git a/music_assistant/server/providers/apple_music/bin/README.md b/music_assistant/server/providers/apple_music/bin/README.md deleted file mode 100644 index a710ed61..00000000 --- a/music_assistant/server/providers/apple_music/bin/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Content Decryption Module (CDM) -You need a custom CDM if you would like to playback Apple music on your local machine. The music provider expects two files to be present in your local user folder `/usr/local/bin/widevine_cdm` : - -1. client_id.bin -2. private_key.pem - -These two files allow Music Assistant to decrypt Widevine protected songs. More info on how you can obtain your own CDM files can be found [here](https://www.ismailzai.com/blog/picking-the-widevine-locks). diff --git a/music_assistant/server/providers/apple_music/icon.svg b/music_assistant/server/providers/apple_music/icon.svg deleted file mode 100644 index ef11384b..00000000 --- a/music_assistant/server/providers/apple_music/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/music_assistant/server/providers/apple_music/manifest.json b/music_assistant/server/providers/apple_music/manifest.json deleted file mode 100644 index 44d212f5..00000000 --- a/music_assistant/server/providers/apple_music/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "apple_music", - "name": "Apple Music", - "description": "Support for the Apple Music streaming provider in Music Assistant.", - "codeowners": ["@MarvinSchenkel"], - "requirements": ["pywidevine==1.8.0"], - "documentation": "https://music-assistant.io/music-providers/apple-music/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/bluesound/__init__.py b/music_assistant/server/providers/bluesound/__init__.py deleted file mode 100644 index 3a7b3901..00000000 --- a/music_assistant/server/providers/bluesound/__init__.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Bluesound Player Provider for BluOS players to work with Music Assistant.""" - -from __future__ import annotations - -import asyncio -import time -from typing import TYPE_CHECKING, TypedDict - -from pyblu import Player as BluosPlayer -from pyblu import Status, SyncStatus -from zeroconf import ServiceStateChange - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_ENABLE_ICY_METADATA, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, - ConfigEntry, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.util import ( - get_port_from_zeroconf, - get_primary_ip_address_from_zeroconf, -) -from music_assistant.server.models.player_provider import PlayerProvider - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -PLAYER_FEATURES_BASE = { - PlayerFeature.SYNC, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.PAUSE, -} - -PLAYBACK_STATE_MAP = { - "play": PlayerState.PLAYING, - "stream": PlayerState.PLAYING, - "stop": PlayerState.IDLE, - "pause": PlayerState.PAUSED, - "connecting": PlayerState.IDLE, -} - -PLAYBACK_STATE_POLL_MAP = { - "play": PlayerState.PLAYING, - "stream": PlayerState.PLAYING, - "stop": PlayerState.IDLE, - "pause": PlayerState.PAUSED, - "connecting": "CONNECTING", -} - -SOURCE_LINE_IN = "line_in" -SOURCE_AIRPLAY = "airplay" -SOURCE_SPOTIFY = "spotify" -SOURCE_UNKNOWN = "unknown" -SOURCE_RADIO = "radio" -POLL_STATE_STATIC = "static" -POLL_STATE_DYNAMIC = "dynamic" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize BluOS instance with given configuration.""" - return BluesoundPlayerProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """Set up legacy BluOS devices.""" - # ruff: noqa: ARG001 - return () - - -class BluesoundDiscoveryInfo(TypedDict): - """Template for MDNS discovery info.""" - - _objectType: str - ip_address: str - port: str - mac: str - model: str - zs: bool - - -class BluesoundPlayer: - """Holds the details of the (discovered) BluOS player.""" - - def __init__( - self, - prov: BluesoundPlayerProvider, - player_id: str, - discovery_info: BluesoundDiscoveryInfo, - ip_address: str, - port: int, - ) -> None: - """Initialize the BluOS Player.""" - self.port = port - self.prov = prov - self.mass = prov.mass - self.player_id = player_id - self.discovery_info = discovery_info - self.ip_address = ip_address - self.logger = prov.logger.getChild(player_id) - self.connected: bool = True - self.client = BluosPlayer(self.ip_address, self.port, self.mass.http_session) - self.sync_status = SyncStatus - self.status = Status - self.poll_state = POLL_STATE_STATIC - self.dynamic_poll_count: int = 0 - self.mass_player: Player | None = None - self._listen_task: asyncio.Task | None = None - - async def disconnect(self) -> None: - """Disconnect the BluOS client and cleanup.""" - if self._listen_task and not self._listen_task.done(): - self._listen_task.cancel() - if self.client: - await self.client.close() - self.connected = False - self.logger.debug("Disconnected from player API") - - async def update_attributes(self) -> None: - """Update the BluOS player attributes.""" - self.logger.debug("updating %s attributes", self.player_id) - if self.dynamic_poll_count > 0: - self.dynamic_poll_count -= 1 - - self.sync_status = await self.client.sync_status() - self.status = await self.client.status() - - # Update timing - self.mass_player.elapsed_time = self.status.seconds - self.mass_player.elapsed_time_last_updated = time.time() - - if not self.mass_player: - return - if self.sync_status.volume == -1: - self.mass_player.volume_level = 100 - else: - self.mass_player.volume_level = self.sync_status.volume - self.mass_player.volume_muted = self.status.mute - - self.logger.log( - VERBOSE_LOG_LEVEL, - "Speaker state: %s vs reported state: %s", - PLAYBACK_STATE_POLL_MAP[self.status.state], - self.mass_player.state, - ) - - if ( - self.poll_state == POLL_STATE_DYNAMIC and self.dynamic_poll_count <= 0 - ) or self.mass_player.state == PLAYBACK_STATE_POLL_MAP[self.status.state]: - self.logger.debug("Changing bluos poll state from %s to static", self.poll_state) - self.poll_state = POLL_STATE_STATIC - self.mass_player.poll_interval = 30 - self.mass.players.update(self.player_id) - - if self.status.state == "stream": - mass_active = self.mass.streams.base_url - elif self.status.state == "stream" and self.status.input_id == "input0": - self.mass_player.active_source = SOURCE_LINE_IN - elif self.status.state == "stream" and self.status.input_id == "Airplay": - self.mass_player.active_source = SOURCE_AIRPLAY - elif self.status.state == "stream" and self.status.input_id == "Spotify": - self.mass_player.active_source = SOURCE_SPOTIFY - elif self.status.state == "stream" and self.status.input_id == "RadioParadise": - self.mass_player.active_source = SOURCE_RADIO - elif self.status.state == "stream" and (mass_active not in self.status.stream_url): - self.mass_player.active_source = SOURCE_UNKNOWN - - # TODO check pair status - - # TODO fix pairing - - if self.sync_status.master is None: - if self.sync_status.slaves: - self.mass_player.group_childs = ( - self.sync_status.slaves if len(self.sync_status.slaves) > 1 else set() - ) - self.mass_player.synced_to = None - - if self.status.state == "stream": - self.mass_player.current_media = PlayerMedia( - uri=self.status.stream_url, - title=self.status.name, - artist=self.status.artist, - album=self.status.album, - image_url=self.status.image, - ) - else: - self.mass_player.current_media = None - - else: - self.mass_player.group_childs = set() - self.mass_player.synced_to = self.sync_status.master - self.mass_player.active_source = self.sync_status.master - - self.mass_player.state = PLAYBACK_STATE_MAP[self.status.state] - self.mass.players.update(self.player_id) - - -class BluesoundPlayerProvider(PlayerProvider): - """Bluos compatible player provider, providing support for bluesound speakers.""" - - bluos_players: dict[str, BluesoundPlayer] - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.bluos_players: dict[str, BluesoundPlayer] = {} - - async def on_mdns_service_state_change( - self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None - ) -> None: - """Handle MDNS service state callback for BluOS.""" - name = name.split(".", 1)[0] - self.player_id = info.decoded_properties["mac"] - # Handle removed player - - if state_change == ServiceStateChange.Removed: - # Check if the player manager has an existing entry for this player - if mass_player := self.mass.players.get(self.player_id): - # The player has become unavailable - self.logger.debug("Player offline: %s", mass_player.display_name) - mass_player.available = False - self.mass.players.update(self.player_id) - return - - if bluos_player := self.bluos_players.get(self.player_id): - if mass_player := self.mass.players.get(self.player_id): - cur_address = get_primary_ip_address_from_zeroconf(info) - cur_port = get_port_from_zeroconf(info) - if cur_address and cur_address != mass_player.device_info.address: - self.logger.debug( - "Address updated to %s for player %s", cur_address, mass_player.display_name - ) - bluos_player.ip_address = cur_address - bluos_player.port = cur_port - mass_player.device_info = DeviceInfo( - model=mass_player.device_info.model, - manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), - ) - if not mass_player.available: - self.logger.debug("Player back online: %s", mass_player.display_name) - bluos_player.client.sync() - bluos_player.discovery_info = info - self.mass.players.update(self.player_id) - return - # handle new player - cur_address = get_primary_ip_address_from_zeroconf(info) - cur_port = get_port_from_zeroconf(info) - self.logger.debug("Discovered device %s on %s", name, cur_address) - - self.bluos_players[self.player_id] = bluos_player = BluesoundPlayer( - self, self.player_id, discovery_info=info, ip_address=cur_address, port=cur_port - ) - - bluos_player.mass_player = mass_player = Player( - player_id=self.player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=name, - available=True, - powered=True, - device_info=DeviceInfo( - model="BluOS speaker", - manufacturer="Bluesound", - address=cur_address, - ), - # Set the supported features for this player - supported_features=( - PlayerFeature.VOLUME_SET, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.PAUSE, - ), - needs_poll=True, - poll_interval=30, - ) - await self.mass.players.register(mass_player) - - # TODO sync - await bluos_player.update_attributes() - self.mass.players.update(self.player_id) - - async def get_player_config_entries( - self, - player_id: str, - ) -> tuple[ConfigEntry, ...]: - """Return Config Entries for the given player.""" - base_entries = await super().get_player_config_entries(self.player_id) - if not self.bluos_players.get(player_id): - # TODO fix player entries - return (*base_entries, CONF_ENTRY_CROSSFADE) - return ( - *base_entries, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_ENABLE_ICY_METADATA, - ) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - play_state = await bluos_player.client.stop(timeout=1) - if play_state == "stop": - bluos_player.poll_state = POLL_STATE_DYNAMIC - bluos_player.dynamic_poll_count = 6 - bluos_player.mass_player.poll_interval = 0.5 - # Update media info then optimistically override playback state and source - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - play_state = await bluos_player.client.play(timeout=1) - if play_state == "stream": - bluos_player.poll_state = POLL_STATE_DYNAMIC - bluos_player.dynamic_poll_count = 6 - bluos_player.mass_player.poll_interval = 0.5 - # Optimistic state, reduces interface lag - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - play_state = await bluos_player.client.pause(timeout=1) - if play_state == "pause": - bluos_player.poll_state = POLL_STATE_DYNAMIC - bluos_player.dynamic_poll_count = 6 - bluos_player.mass_player.poll_interval = 0.5 - self.logger.debug("Set BluOS state to %s", play_state) - # Optimistic state, reduces interface lag - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - await bluos_player.client.volume(level=volume_level, timeout=1) - self.logger.debug("Set BluOS speaker volume to %s", volume_level) - mass_player = self.mass.players.get(player_id) - # Optimistic state, reduces interface lag - mass_player.volume_level = volume_level - await bluos_player.update_attributes() - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - await bluos_player.client.volume(mute=muted) - # Optimistic state, reduces interface lag - mass_player = self.mass.players.get(player_id) - mass_player.volume_mute = muted - await bluos_player.update_attributes() - - async def play_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle PLAY MEDIA for BluOS player using the provided URL.""" - self.logger.debug("Play_media called") - if bluos_player := self.bluos_players[player_id]: - self.mass.players.update(player_id) - play_state = await bluos_player.client.play_url(media.uri, timeout=1) - # Enable dynamic polling - if play_state == "stream": - bluos_player.poll_state = POLL_STATE_DYNAMIC - bluos_player.dynamic_poll_count = 6 - bluos_player.mass_player.poll_interval = 0.5 - self.logger.debug("Set BluOS state to %s", play_state) - await bluos_player.update_attributes() - - # Optionally, handle the playback_state or additional logic here - if play_state in ("PlayerUnexpectedResponseError", "PlayerUnreachableError"): - raise PlayerCommandFailed("Failed to start playback.") - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - if bluos_player := self.bluos_players[player_id]: - await bluos_player.update_attributes() - - # TODO fix sync & unsync - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for BluOS player.""" - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for BluOS player.""" - if bluos_player := self.bluos_players[player_id]: - await bluos_player.client.player.leave_group() diff --git a/music_assistant/server/providers/bluesound/icon.svg b/music_assistant/server/providers/bluesound/icon.svg deleted file mode 100644 index 2cb9d37b..00000000 --- a/music_assistant/server/providers/bluesound/icon.svg +++ /dev/null @@ -1,44 +0,0 @@ - - diff --git a/music_assistant/server/providers/bluesound/manifest.json b/music_assistant/server/providers/bluesound/manifest.json deleted file mode 100644 index 3379858e..00000000 --- a/music_assistant/server/providers/bluesound/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "player", - "domain": "bluesound", - "name": "Bluesound", - "description": "BluOS Player provider for Music Assistant.", - "codeowners": ["@cyanogenbot"], - "requirements": ["pyblu==1.0.4"], - "documentation": "https://music-assistant.io/player-support/bluesound/", - "mdns_discovery": ["_musc._tcp.local."] -} diff --git a/music_assistant/server/providers/builtin/__init__.py b/music_assistant/server/providers/builtin/__init__.py deleted file mode 100644 index a5a4b0ed..00000000 --- a/music_assistant/server/providers/builtin/__init__.py +++ /dev/null @@ -1,641 +0,0 @@ -"""Built-in/generic provider to handle media from files and (remote) urls.""" - -from __future__ import annotations - -import asyncio -import os -import time -from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, NotRequired, TypedDict, cast - -import aiofiles -import shortuuid - -from music_assistant.common.helpers.uri import parse_uri -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( - CacheCategory, - ConfigEntryType, - ContentType, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - InvalidDataError, - MediaNotFoundError, - ProviderUnavailableError, -) -from music_assistant.common.models.media_items import ( - Artist, - AudioFormat, - MediaItemImage, - MediaItemMetadata, - MediaItemType, - Playlist, - ProviderMapping, - Radio, - Track, - UniqueList, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import MASS_LOGO, RESOURCES_DIR, VARIOUS_ARTISTS_FANART -from music_assistant.server.helpers.tags import AudioTags, parse_tags -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -class StoredItem(TypedDict): - """Definition of an media item (for the builtin provider) stored in persistent storage.""" - - item_id: str # url or (locally accessible) file path (or id in case of playlist) - name: str - image_url: NotRequired[str] - last_updated: NotRequired[int] - - -CONF_KEY_RADIOS = "stored_radios" -CONF_KEY_TRACKS = "stored_tracks" -CONF_KEY_PLAYLISTS = "stored_playlists" - - -ALL_FAVORITE_TRACKS = "all_favorite_tracks" -RANDOM_ARTIST = "random_artist" -RANDOM_ALBUM = "random_album" -RANDOM_TRACKS = "random_tracks" -RECENTLY_PLAYED = "recently_played" - -BUILTIN_PLAYLISTS = { - ALL_FAVORITE_TRACKS: "All favorited tracks", - RANDOM_ARTIST: "Random Artist (from library)", - RANDOM_ALBUM: "Random Album (from library)", - RANDOM_TRACKS: "500 Random tracks (from library)", - RECENTLY_PLAYED: "Recently played tracks", -} - -COLLAGE_IMAGE_PLAYLISTS = (ALL_FAVORITE_TRACKS, RANDOM_TRACKS) - -DEFAULT_THUMB = MediaItemImage( - type=ImageType.THUMB, - path=MASS_LOGO, - provider="builtin", - remotely_accessible=False, -) - -DEFAULT_FANART = MediaItemImage( - type=ImageType.FANART, - path=VARIOUS_ARTISTS_FANART, - provider="builtin", - remotely_accessible=False, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return BuiltinProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, # noqa: ARG001 - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return tuple( - ConfigEntry( - key=key, - type=ConfigEntryType.BOOLEAN, - label=name, - default_value=True, - category="builtin_playlists", - ) - for key, name in BUILTIN_PLAYLISTS.items() - ) - - -class BuiltinProvider(MusicProvider): - """Built-in/generic provider to handle (manually added) media from files and (remote) urls.""" - - _playlists_dir: str - _playlist_lock: asyncio.Lock - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - self._playlist_lock = asyncio.Lock() - # make sure that our directory with collage images exists - self._playlists_dir = os.path.join(self.mass.storage_path, "playlists") - if not await asyncio.to_thread(os.path.exists, self._playlists_dir): - await asyncio.to_thread(os.mkdir, self._playlists_dir) - await super().loaded_in_mass() - # migrate old image path - # TODO: remove this after 2.3+ release - old_path = ( - "/usr/local/lib/python3.12/site-packages/music_assistant/server/helpers/resources" - ) - new_path = str(RESOURCES_DIR) - query = ( - "UPDATE playlists SET metadata = " - f"REPLACE (metadata, '{old_path}', '{new_path}') " - f"WHERE playlists.metadata LIKE '%{old_path}%'" - ) - if self.mass.music.database: - await self.mass.music.database.execute(query) - await self.mass.music.database.commit() - - @property - def is_streaming_provider(self) -> bool: - """Return True if the provider is a streaming provider.""" - return False - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return ( - ProviderFeature.BROWSE, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_RADIOS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.LIBRARY_RADIOS_EDIT, - ProviderFeature.PLAYLIST_CREATE, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - parsed_item = cast(Track, await self.parse_item(prov_track_id)) - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_TRACKS, []) - if stored_item := next((x for x in stored_items if x["item_id"] == prov_track_id), None): - # always prefer the stored info, such as the name - parsed_item.name = stored_item["name"] - if image_url := stored_item.get("image_url"): - parsed_item.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.domain, - remotely_accessible=image_url.startswith("http"), - ) - ] - ) - return parsed_item - - async def get_radio(self, prov_radio_id: str) -> Radio: - """Get full radio details by id.""" - parsed_item = await self.parse_item(prov_radio_id, force_radio=True) - assert isinstance(parsed_item, Radio) - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_RADIOS, []) - if stored_item := next((x for x in stored_items if x["item_id"] == prov_radio_id), None): - # always prefer the stored info, such as the name - parsed_item.name = stored_item["name"] - if image_url := stored_item.get("image_url"): - parsed_item.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.domain, - remotely_accessible=image_url.startswith("http"), - ) - ] - ) - return parsed_item - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - artist = prov_artist_id - # this is here for compatibility reasons only - return Artist( - item_id=artist, - provider=self.domain, - name=artist, - provider_mappings={ - ProviderMapping( - item_id=artist, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=False, - ) - }, - ) - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id.""" - if prov_playlist_id in BUILTIN_PLAYLISTS: - # this is one of our builtin/default playlists - return Playlist( - item_id=prov_playlist_id, - provider=self.instance_id, - name=BUILTIN_PLAYLISTS[prov_playlist_id], - provider_mappings={ - ProviderMapping( - item_id=prov_playlist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - owner="Music Assistant", - is_editable=False, - cache_checksum=str(int(time.time())), - metadata=MediaItemMetadata( - images=UniqueList([DEFAULT_THUMB]) - if prov_playlist_id in COLLAGE_IMAGE_PLAYLISTS - else UniqueList([DEFAULT_THUMB, DEFAULT_FANART]), - ), - ) - # user created universal playlist - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) - stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) - if not stored_item: - raise MediaNotFoundError - playlist = Playlist( - item_id=prov_playlist_id, - provider=self.instance_id, - name=stored_item["name"], - provider_mappings={ - ProviderMapping( - item_id=prov_playlist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - owner="Music Assistant", - is_editable=True, - ) - playlist.cache_checksum = str(stored_item.get("last_updated")) - if image_url := stored_item.get("image_url"): - playlist.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.domain, - remotely_accessible=image_url.startswith("http"), - ) - ] - ) - return playlist - - async def get_item(self, media_type: MediaType, prov_item_id: str) -> MediaItemType: - """Get single MediaItem from provider.""" - if media_type == MediaType.ARTIST: - return await self.get_artist(prov_item_id) - if media_type == MediaType.TRACK: - return await self.get_track(prov_item_id) - if media_type == MediaType.RADIO: - return await self.get_radio(prov_item_id) - if media_type == MediaType.PLAYLIST: - return await self.get_playlist(prov_item_id) - if media_type == MediaType.UNKNOWN: - return await self.parse_item(prov_item_id) - raise NotImplementedError - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_TRACKS, []) - for item in stored_items: - yield await self.get_track(item["item_id"]) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve library/subscribed playlists from the provider.""" - # return user stored playlists - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) - for item in stored_items: - yield await self.get_playlist(item["item_id"]) - # return builtin playlists - for item_id in BUILTIN_PLAYLISTS: - if self.config.get_value(item_id) is False: - continue - yield await self.get_playlist(item_id) - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_RADIOS, []) - for item in stored_items: - yield await self.get_radio(item["item_id"]) - - async def library_add(self, item: MediaItemType) -> bool: - """Add item to provider's library. Return true on success.""" - if item.media_type == MediaType.TRACK: - key = CONF_KEY_TRACKS - elif item.media_type == MediaType.RADIO: - key = CONF_KEY_RADIOS - else: - return False - stored_item = StoredItem(item_id=item.item_id, name=item.name) - if item.image: - stored_item["image_url"] = item.image.path - stored_items: list[StoredItem] = self.mass.config.get(key, []) - # filter out existing - stored_items = [x for x in stored_items if x["item_id"] != item.item_id] - stored_items.append(stored_item) - self.mass.config.set(key, stored_items) - return True - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove item from provider's library. Return true on success.""" - if media_type == MediaType.PLAYLIST and prov_item_id in BUILTIN_PLAYLISTS: - # user wants to disable/remove one of our builtin playlists - # to prevent it comes back, we mark it as disabled in config - self.mass.config.set_raw_provider_config_value(self.instance_id, prov_item_id, False) - return True - if media_type == MediaType.TRACK: - # regular manual track URL/path - key = CONF_KEY_TRACKS - elif media_type == MediaType.RADIO: - # regular manual radio URL/path - key = CONF_KEY_RADIOS - elif media_type == MediaType.PLAYLIST: - # manually added (multi provider) playlist removal - key = CONF_KEY_PLAYLISTS - else: - return False - stored_items: list[StoredItem] = self.mass.config.get(key, []) - stored_items = [x for x in stored_items if x["item_id"] != prov_item_id] - self.mass.config.set(key, stored_items) - return True - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - if page > 0: - # paging not supported, we always return the whole list at once - return [] - if prov_playlist_id in BUILTIN_PLAYLISTS: - return await self._get_builtin_playlist_tracks(prov_playlist_id) - # user created universal playlist - result: list[Track] = [] - playlist_items = await self._read_playlist_file_items(prov_playlist_id) - for index, uri in enumerate(playlist_items, 1): - try: - media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) - media_controller = self.mass.music.get_controller(media_type) - # prefer item already in the db - track = await media_controller.get_library_item_by_prov_id( - item_id, provider_instance_id_or_domain - ) - if track is None: - # get the provider item and not the full track from a regular 'get' call - # as we only need basic track info here - track = await media_controller.get_provider_item( - item_id, provider_instance_id_or_domain - ) - assert isinstance(track, Track) - track.position = index - result.append(track) - except (MediaNotFoundError, InvalidDataError, ProviderUnavailableError) as err: - self.logger.warning( - "Skipping %s in playlist %s: %s", uri, prov_playlist_id, str(err) - ) - return result - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - playlist_items = await self._read_playlist_file_items(prov_playlist_id) - for uri in prov_track_ids: - if uri not in playlist_items: - playlist_items.append(uri) - # store playlist file - await self._write_playlist_file_items(prov_playlist_id, playlist_items) - # mark last_updated on playlist object - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) - stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) - if stored_item: - stored_item["last_updated"] = int(time.time()) - self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - playlist_items = await self._read_playlist_file_items(prov_playlist_id) - # remove items by index - for i in sorted(positions_to_remove, reverse=True): - del playlist_items[i - 1] - # store playlist file - await self._write_playlist_file_items(prov_playlist_id, playlist_items) - # mark last_updated on playlist object - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) - stored_item = next((x for x in stored_items if x["item_id"] == prov_playlist_id), None) - if stored_item: - stored_item["last_updated"] = int(time.time()) - self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) - - async def create_playlist(self, name: str) -> Playlist: - """Create a new playlist on provider with given name.""" - item_id = shortuuid.random(8) - stored_item = StoredItem(item_id=item_id, name=name) - stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_PLAYLISTS, []) - stored_items.append(stored_item) - self.mass.config.set(CONF_KEY_PLAYLISTS, stored_items) - return await self.get_playlist(item_id) - - async def parse_item( - self, - url: str, - force_refresh: bool = False, - force_radio: bool = False, - ) -> Track | Radio: - """Parse plain URL to MediaItem of type Radio or Track.""" - try: - media_info = await self._get_media_info(url, force_refresh) - except Exception as err: - raise MediaNotFoundError from err - is_radio = media_info.get("icyname") or not media_info.duration - provider_mappings = { - ProviderMapping( - item_id=url, - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(media_info.format), - sample_rate=media_info.sample_rate, - bit_depth=media_info.bits_per_sample, - bit_rate=media_info.bit_rate, - ), - ) - } - media_item: Track | Radio - if is_radio or force_radio: - # treat as radio - media_item = Radio( - item_id=url, - provider=self.domain, - name=media_info.get("icyname") - or media_info.get("programtitle") - or media_info.title - or url, - provider_mappings=provider_mappings, - ) - else: - media_item = Track( - item_id=url, - provider=self.domain, - name=media_info.title or url, - duration=int(media_info.duration or 0), - artists=UniqueList( - [await self.get_artist(artist) for artist in media_info.artists] - ), - provider_mappings=provider_mappings, - ) - - if media_info.has_cover_image: - media_item.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=url, - provider=self.domain, - remotely_accessible=False, - ) - ] - ) - return media_item - - async def _get_media_info(self, url: str, force_refresh: bool = False) -> AudioTags: - """Retrieve mediainfo for url.""" - cache_category = CacheCategory.MEDIA_INFO - cache_base_key = self.lookup_key - # do we have some cached info for this url ? - cached_info = await self.mass.cache.get( - url, category=cache_category, base_key=cache_base_key - ) - if cached_info and not force_refresh: - return AudioTags.parse(cached_info) - # parse info with ffprobe (and store in cache) - media_info = await parse_tags(url) - if "authSig" in url: - media_info.has_cover_image = False - await self.mass.cache.set( - url, media_info.raw, category=cache_category, base_key=cache_base_key - ) - return media_info - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track/radio.""" - media_info = await self._get_media_info(item_id) - is_radio = media_info.get("icy-name") or not media_info.duration - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(media_info.format), - sample_rate=media_info.sample_rate, - bit_depth=media_info.bits_per_sample, - channels=media_info.channels, - ), - media_type=MediaType.RADIO if is_radio else MediaType.TRACK, - stream_type=StreamType.HTTP, - path=item_id, - can_seek=not is_radio, - ) - - async def _get_builtin_playlist_random_favorite_tracks(self) -> list[Track]: - result: list[Track] = [] - res = await self.mass.music.tracks.library_items( - favorite=True, limit=250000, order_by="random_play_count" - ) - for idx, item in enumerate(res, 1): - item.position = idx - result.append(item) - return result - - async def _get_builtin_playlist_random_tracks(self) -> list[Track]: - result: list[Track] = [] - res = await self.mass.music.tracks.library_items(limit=500, order_by="random_play_count") - for idx, item in enumerate(res, 1): - item.position = idx - result.append(item) - return result - - async def _get_builtin_playlist_random_album(self) -> list[Track]: - for in_library_only in (True, False): - for min_tracks_required in (10, 5, 1): - for random_album in await self.mass.music.albums.library_items( - limit=25, order_by="random" - ): - tracks = await self.mass.music.albums.tracks( - random_album.item_id, random_album.provider, in_library_only=in_library_only - ) - if len(tracks) < min_tracks_required: - continue - for idx, track in enumerate(tracks, 1): - track.position = idx - return tracks - return [] - - async def _get_builtin_playlist_random_artist(self) -> list[Track]: - for in_library_only in (True, False): - for min_tracks_required in (25, 10, 5, 1): - for random_artist in await self.mass.music.artists.library_items( - limit=25, order_by="random" - ): - tracks = await self.mass.music.artists.tracks( - random_artist.item_id, - random_artist.provider, - in_library_only=in_library_only, - ) - if len(tracks) < min_tracks_required: - continue - for idx, track in enumerate(tracks, 1): - track.position = idx - return tracks - return [] - - async def _get_builtin_playlist_recently_played(self) -> list[Track]: - result: list[Track] = [] - recent_tracks = await self.mass.music.recently_played(100, [MediaType.TRACK]) - for idx, track in enumerate(recent_tracks, 1): - assert isinstance(track, Track) - track.position = idx - result.append(track) - return result - - async def _get_builtin_playlist_tracks(self, builtin_playlist_id: str) -> list[Track]: - """Get all playlist tracks for given builtin playlist id.""" - try: - return await { - ALL_FAVORITE_TRACKS: self._get_builtin_playlist_random_favorite_tracks, - RANDOM_TRACKS: self._get_builtin_playlist_random_tracks, - RANDOM_ALBUM: self._get_builtin_playlist_random_album, - RANDOM_ARTIST: self._get_builtin_playlist_random_artist, - RECENTLY_PLAYED: self._get_builtin_playlist_recently_played, - }[builtin_playlist_id]() - except KeyError: - raise MediaNotFoundError(f"No built in playlist: {builtin_playlist_id}") - - async def _read_playlist_file_items(self, playlist_id: str) -> list[str]: - """Return lines of a playlist file.""" - playlist_file = os.path.join(self._playlists_dir, playlist_id) - if not await asyncio.to_thread(os.path.isfile, playlist_file): - return [] - async with ( - self._playlist_lock, - aiofiles.open(playlist_file, encoding="utf-8") as _file, - ): - lines = await _file.readlines() - return [x.strip() for x in lines] - - async def _write_playlist_file_items(self, playlist_id: str, lines: list[str]) -> None: - """Return lines of a playlist file.""" - playlist_file = os.path.join(self._playlists_dir, playlist_id) - async with ( - self._playlist_lock, - aiofiles.open(playlist_file, "w", encoding="utf-8") as _file, - ): - await _file.write("\n".join(lines)) diff --git a/music_assistant/server/providers/builtin/icon.svg b/music_assistant/server/providers/builtin/icon.svg deleted file mode 100644 index 845920ca..00000000 --- a/music_assistant/server/providers/builtin/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/builtin/manifest.json b/music_assistant/server/providers/builtin/manifest.json deleted file mode 100644 index dc489276..00000000 --- a/music_assistant/server/providers/builtin/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "music", - "domain": "builtin", - "name": "Music Assistant", - "description": "Built-in/generic provider that handles generic urls and playlists.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/music-providers/builtin/", - "multi_instance": false, - "builtin": true, - "allow_disable": false -} diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py deleted file mode 100644 index 6d13fa42..00000000 --- a/music_assistant/server/providers/chromecast/__init__.py +++ /dev/null @@ -1,750 +0,0 @@ -"""Chromecast Player provider for Music Assistant, utilizing the pychromecast library.""" - -from __future__ import annotations - -import asyncio -import contextlib -import logging -import threading -import time -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any -from uuid import UUID - -import pychromecast -from pychromecast.controllers.media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE, MediaController -from pychromecast.controllers.multizone import MultizoneController, MultizoneManager -from pychromecast.discovery import CastBrowser, SimpleCastListener -from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED - -from music_assistant.common.models.config_entries import ( - BASE_PLAYER_CONFIG_ENTRIES, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_ENFORCE_MP3, - ConfigEntry, - ConfigValueType, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import MediaType, PlayerFeature, PlayerState, PlayerType -from music_assistant.common.models.errors import PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import ( - CONF_ENFORCE_MP3, - CONF_PLAYERS, - MASS_LOGO_ONLINE, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.models.player_provider import PlayerProvider - -from .helpers import CastStatusListener, ChromecastInfo - -if TYPE_CHECKING: - from pychromecast.controllers.media import MediaStatus - from pychromecast.controllers.receiver import CastStatus - from pychromecast.models import CastInfo - from pychromecast.socket_client import ConnectionStatus - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -PLAYER_CONFIG_ENTRIES = ( - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3, -) - -# originally/officially cast supports 96k sample rate (even for groups) -# but it seems a (recent?) update broke this ?! -# For now only set safe default values and let the user try out higher values -CONF_ENTRY_SAMPLE_RATES_CAST = create_sample_rates_config_entry(96000, 24, 48000, 24) -CONF_ENTRY_SAMPLE_RATES_CAST_GROUP = create_sample_rates_config_entry(96000, 24, 44100, 16) - - -MASS_APP_ID = "C35B0678" - - -# Monkey patch the Media controller here to store the queue items -_patched_process_media_status_org = MediaController._process_media_status - - -def _patched_process_media_status(self, data) -> None: - """Process STATUS message(s) of the media controller.""" - _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 - - -MediaController._process_media_status = _patched_process_media_status - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return ChromecastProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return () # we do not have any config entries (yet) - - -@dataclass -class CastPlayer: - """Wrapper around Chromecast with some additional attributes.""" - - player_id: str - cast_info: ChromecastInfo - cc: pychromecast.Chromecast - player: Player - status_listener: CastStatusListener | None = None - mz_controller: MultizoneController | None = None - active_group: str | None = None - last_poll: float = 0 - flow_meta_checksum: str | None = None - - -class ChromecastProvider(PlayerProvider): - """Player provider for Chromecast based players.""" - - mz_mgr: MultizoneManager | None = None - browser: CastBrowser | None = None - castplayers: dict[str, CastPlayer] - _discover_lock: threading.Lock - - def __init__( - self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig - ) -> None: - """Handle async initialization of the provider.""" - super().__init__(mass, manifest, config) - self._discover_lock = threading.Lock() - self.castplayers = {} - self.mz_mgr = MultizoneManager() - self.browser = CastBrowser( - SimpleCastListener( - add_callback=self._on_chromecast_discovered, - remove_callback=self._on_chromecast_removed, - update_callback=self._on_chromecast_discovered, - ), - self.mass.aiozc.zeroconf, - ) - # set-up pychromecast logging - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("pychromecast").setLevel(logging.DEBUG) - else: - logging.getLogger("pychromecast").setLevel(self.logger.level + 10) - - async def discover_players(self) -> None: - """Discover Cast players on the network.""" - # start discovery in executor - await self.mass.loop.run_in_executor(None, self.browser.start_discovery) - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - if not self.browser: - return - - # stop discovery - def stop_discovery() -> None: - """Stop the chromecast discovery threads.""" - if self.browser._zc_browser: - with contextlib.suppress(RuntimeError): - self.browser._zc_browser.cancel() - - self.browser.host_browser.stop.set() - self.browser.host_browser.join() - - await self.mass.loop.run_in_executor(None, stop_discovery) - # stop all chromecasts - for castplayer in list(self.castplayers.values()): - await self._disconnect_chromecast(castplayer) - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - cast_player = self.castplayers.get(player_id) - if cast_player and cast_player.player.type == PlayerType.GROUP: - return ( - *BASE_PLAYER_CONFIG_ENTRIES, - *PLAYER_CONFIG_ENTRIES, - CONF_ENTRY_SAMPLE_RATES_CAST_GROUP, - ) - base_entries = await super().get_player_config_entries(player_id) - return (*base_entries, *PLAYER_CONFIG_ENTRIES, CONF_ENTRY_SAMPLE_RATES_CAST) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.media_controller.stop) - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.media_controller.play) - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.media_controller.pause) - - async def cmd_next(self, player_id: str) -> None: - """Handle NEXT TRACK command for given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.media_controller.queue_next) - - async def cmd_previous(self, player_id: str) -> None: - """Handle PREVIOUS TRACK command for given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.media_controller.queue_prev) - - async def cmd_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player.""" - castplayer = self.castplayers[player_id] - if powered: - await self._launch_app(castplayer) - else: - castplayer.player.active_group = None - castplayer.player.active_source = None - await asyncio.to_thread(castplayer.cc.quit_app) - # optimistically update the group childs - if castplayer.player.type == PlayerType.GROUP: - active_group = castplayer.player.active_group or castplayer.player.player_id - for child_id in castplayer.player.group_childs: - if child := self.castplayers.get(child_id): - child.player.powered = powered - child.player.active_group = active_group if powered else None - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.set_volume, volume_level / 100) - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - castplayer = self.castplayers[player_id] - await asyncio.to_thread(castplayer.cc.set_volume_muted, muted) - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - castplayer = self.castplayers[player_id] - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): - media.uri = media.uri.replace(".flac", ".mp3") - queuedata = { - "type": "LOAD", - "media": self._create_cc_media_item(media), - } - # make sure that our media controller app is launched - await self._launch_app(castplayer) - # send queue info to the CC - media_controller = castplayer.cc.media_controller - await asyncio.to_thread(media_controller.send_message, data=queuedata, inc_session_id=True) - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next item on the player.""" - castplayer = self.castplayers[player_id] - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): - media.uri = media.uri.replace(".flac", ".mp3") - next_item_id = None - 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 - if not cur_item_found: - continue - next_item_id = item["itemId"] - # check if the next queue item isn't already queued - if item.get("media", {}).get("customData", {}).get("uri") == media.uri: - return - queuedata = { - "type": "QUEUE_INSERT", - "insertBefore": next_item_id, - "items": [ - { - "autoplay": True, - "startTime": 0, - "preloadTime": 0, - "media": self._create_cc_media_item(media), - } - ], - } - media_controller = castplayer.cc.media_controller - queuedata["mediaSessionId"] = media_controller.status.media_session_id - self.mass.create_task(media_controller.send_message, data=queuedata, inc_session_id=True) - self.logger.debug( - "Enqued next track (%s) to player %s", - media.title or media.uri, - castplayer.player.display_name, - ) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - castplayer = self.castplayers[player_id] - # only update status of media controller if player is on - if not castplayer.player.powered: - return - if not castplayer.cc.media_controller.is_active: - return - try: - now = time.time() - if (now - castplayer.last_poll) >= 60: - castplayer.last_poll = now - await asyncio.to_thread(castplayer.cc.media_controller.update_status) - await self.update_flow_metadata(castplayer) - except ConnectionResetError as err: - raise PlayerUnavailableError from err - - ### Discovery callbacks - - def _on_chromecast_discovered(self, uuid, _) -> None: - """Handle Chromecast discovered callback.""" - if self.mass.closing: - return - - with self._discover_lock: - disc_info: CastInfo = self.browser.devices[uuid] - - if disc_info.uuid is None: - self.logger.error("Discovered chromecast without uuid %s", disc_info) - return - - player_id = str(disc_info.uuid) - - enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True) - if not enabled: - self.logger.debug("Ignoring disabled player: %s", player_id) - return - - self.logger.debug("Discovered new or updated chromecast %s", disc_info) - - castplayer = self.castplayers.get(player_id) - if castplayer: - # if player was already added, the player will take care of reconnects itself. - castplayer.cast_info.update(disc_info) - self.mass.loop.call_soon_threadsafe(self.mass.players.update, player_id) - return - # new player discovered - cast_info = ChromecastInfo.from_cast_info(disc_info) - cast_info.fill_out_missing_chromecast_info(self.mass.aiozc.zeroconf) - if cast_info.is_dynamic_group: - self.logger.debug("Discovered a dynamic cast group which will be ignored.") - return - if cast_info.is_multichannel_child: - self.logger.debug( - "Discovered a passive (multichannel) endpoint which will be ignored." - ) - return - - # Disable TV's by default - # (can be enabled manually by the user) - enabled_by_default = True - for exclude in ("tv", "/12", "PUS", "OLED"): - if exclude.lower() in cast_info.friendly_name.lower(): - enabled_by_default = False - - if cast_info.is_audio_group and cast_info.is_multichannel_group: - player_type = PlayerType.STEREO_PAIR - elif cast_info.is_audio_group: - player_type = PlayerType.GROUP - else: - player_type = PlayerType.PLAYER - # Instantiate chromecast object - castplayer = CastPlayer( - player_id, - cast_info=cast_info, - cc=pychromecast.get_chromecast_from_cast_info( - disc_info, - self.mass.aiozc.zeroconf, - ), - player=Player( - player_id=player_id, - provider=self.instance_id, - type=player_type, - name=cast_info.friendly_name, - available=False, - powered=False, - device_info=DeviceInfo( - model=cast_info.model_name, - address=f"{cast_info.host}:{cast_info.port}", - manufacturer=cast_info.manufacturer, - ), - supported_features=( - PlayerFeature.POWER, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.VOLUME_SET, - PlayerFeature.PAUSE, - PlayerFeature.NEXT_PREVIOUS, - PlayerFeature.ENQUEUE, - ), - enabled_by_default=enabled_by_default, - needs_poll=True, - ), - ) - self.castplayers[player_id] = castplayer - - castplayer.status_listener = CastStatusListener(self, castplayer, self.mz_mgr) - if castplayer.player.type == PlayerType.GROUP: - mz_controller = MultizoneController(cast_info.uuid) - castplayer.cc.register_handler(mz_controller) - castplayer.mz_controller = mz_controller - - castplayer.cc.start() - asyncio.run_coroutine_threadsafe( - self.mass.players.register_or_update(castplayer.player), loop=self.mass.loop - ) - - def _on_chromecast_removed(self, uuid, service, cast_info) -> None: - """Handle zeroconf discovery of a removed Chromecast.""" - player_id = str(service[1]) - friendly_name = service[3] - self.logger.debug("Chromecast removed: %s - %s", friendly_name, player_id) - # we ignore this event completely as the Chromecast socket client handles this itself - - ### Callbacks from Chromecast Statuslistener - - def on_new_cast_status(self, castplayer: CastPlayer, status: CastStatus) -> None: - """Handle updated CastStatus.""" - if status is None: - return # guard - self.logger.log( - VERBOSE_LOG_LEVEL, - "Received cast status for %s - app_id: %s - volume: %s", - castplayer.player.display_name, - status.app_id, - status.volume_level, - ) - # handle stereo pairs - if castplayer.cast_info.is_multichannel_group: - castplayer.player.type = PlayerType.STEREO_PAIR - castplayer.player.group_childs = set() - # handle cast groups - if castplayer.cast_info.is_audio_group and not castplayer.cast_info.is_multichannel_group: - castplayer.player.type = PlayerType.GROUP - castplayer.player.group_childs = { - str(UUID(x)) for x in castplayer.mz_controller.members - } - castplayer.player.supported_features = ( - PlayerFeature.POWER, - PlayerFeature.VOLUME_SET, - PlayerFeature.PAUSE, - PlayerFeature.ENQUEUE, - ) - - # update player status - castplayer.player.name = castplayer.cast_info.friendly_name - castplayer.player.volume_level = int(status.volume_level * 100) - castplayer.player.volume_muted = status.volume_muted - new_powered = ( - castplayer.cc.app_id is not None and castplayer.cc.app_id != pychromecast.IDLE_APP_ID - ) - if ( - castplayer.player.powered - and not new_powered - and castplayer.player.type == PlayerType.GROUP - ): - # group is being powered off, update group childs - for child_id in castplayer.player.group_childs: - if child := self.castplayers.get(child_id): - child.player.powered = False - child.player.active_group = None - child.player.active_source = None - castplayer.player.powered = new_powered - # send update to player manager - self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) - - def on_new_media_status(self, castplayer: CastPlayer, status: MediaStatus) -> None: - """Handle updated MediaStatus.""" - self.logger.log( - VERBOSE_LOG_LEVEL, - "Received media status for %s update: %s", - castplayer.player.display_name, - status.player_state, - ) - # handle castplayer playing from a group - group_player: CastPlayer | None = None - if castplayer.active_group is not None: - if not (group_player := self.castplayers.get(castplayer.active_group)): - return - status = group_player.cc.media_controller.status - - # 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.elapsed_time = status.current_time - - # active source - if group_player: - castplayer.player.active_source = ( - group_player.player.active_source or group_player.player.player_id - ) - castplayer.player.active_group = ( - group_player.player.active_group or group_player.player.player_id - ) - elif castplayer.cc.app_id == MASS_APP_ID: - castplayer.player.active_source = castplayer.player_id - else: - castplayer.player.active_source = castplayer.cc.app_display_name - - if status.content_id and not status.player_is_idle: - castplayer.player.current_media = PlayerMedia( - uri=status.content_id, - title=status.title, - artist=status.artist, - album=status.album_name, - image_url=status.images[0].url if status.images else None, - duration=status.duration, - media_type=MediaType.TRACK, - ) - else: - castplayer.player.current_media = None - - # weird workaround which is needed for multichannel group childs - # (e.g. a stereo pair within a cast group) - # where it does not receive updates from the group, - # so we need to update the group child(s) manually - if castplayer.player.type == PlayerType.GROUP and castplayer.player.powered: - for child_id in castplayer.player.group_childs: - if child := self.castplayers.get(child_id): - if not child.cast_info.is_multichannel_group: - continue - child.player.state = castplayer.player.state - child.player.current_media = castplayer.player.current_media - child.player.elapsed_time = castplayer.player.elapsed_time - child.player.elapsed_time_last_updated = ( - castplayer.player.elapsed_time_last_updated - ) - child.player.active_source = castplayer.player.active_source - child.player.active_group = castplayer.player.active_group - - self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) - - def on_new_connection_status(self, castplayer: CastPlayer, status: ConnectionStatus) -> None: - """Handle updated ConnectionStatus.""" - self.logger.log( - VERBOSE_LOG_LEVEL, - "Received connection status update for %s - status: %s", - castplayer.player.display_name, - status.status, - ) - - if status.status == CONNECTION_STATUS_DISCONNECTED: - castplayer.player.available = False - self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) - return - - new_available = status.status == CONNECTION_STATUS_CONNECTED - if new_available != castplayer.player.available: - self.logger.debug( - "[%s] Cast device availability changed: %s", - castplayer.cast_info.friendly_name, - status.status, - ) - castplayer.player.available = new_available - castplayer.player.device_info = DeviceInfo( - model=castplayer.cast_info.model_name, - address=f"{castplayer.cast_info.host}:{castplayer.cast_info.port}", - manufacturer=castplayer.cast_info.manufacturer, - ) - self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) - if new_available and castplayer.player.type == PlayerType.PLAYER: - # Poll current group status - for group_uuid in self.mz_mgr.get_multizone_memberships(castplayer.cast_info.uuid): - group_media_controller = self.mz_mgr.get_multizone_mediacontroller(group_uuid) - if not group_media_controller: - continue - - ### Helpers / utils - - async def _launch_app(self, castplayer: CastPlayer, app_id: str = MASS_APP_ID) -> None: - """Launch the default Media Receiver App on a Chromecast.""" - event = asyncio.Event() - - if castplayer.cc.app_id == app_id: - return # already active - - def launched_callback(success: bool, response: dict[str, Any] | None) -> None: - self.mass.loop.call_soon_threadsafe(event.set) - - def launch() -> None: - # Quit the previous app before starting splash screen or media player - if castplayer.cc.app_id is not None: - castplayer.cc.quit_app() - self.logger.debug("Launching App %s.", app_id) - castplayer.cc.socket_client.receiver_controller.launch_app( - app_id, - force_launch=True, - callback_function=launched_callback, - ) - - await self.mass.loop.run_in_executor(None, launch) - await event.wait() - - async def _disconnect_chromecast(self, castplayer: CastPlayer) -> None: - """Disconnect Chromecast object if it is set.""" - self.logger.debug("Disconnecting from chromecast socket %s", castplayer.player.display_name) - await self.mass.loop.run_in_executor(None, castplayer.cc.disconnect, 10) - castplayer.mz_controller = None - castplayer.status_listener.invalidate() - castplayer.status_listener = None - self.castplayers.pop(castplayer.player_id, None) - - def _create_cc_media_item(self, media: PlayerMedia) -> dict[str, Any]: - """Create CC media item from MA PlayerMedia.""" - if media.media_type == MediaType.TRACK: - stream_type = STREAM_TYPE_BUFFERED - else: - stream_type = STREAM_TYPE_LIVE - metadata = { - "metadataType": 3, - "albumName": media.album or "", - "songName": media.title or "", - "artist": media.artist or "", - "title": media.title or "", - "images": [{"url": media.image_url}] if media.image_url else None, - } - return { - "contentId": media.uri, - "customData": { - "uri": media.uri, - "queue_item_id": media.uri, - "deviceName": "Music Assistant", - }, - "contentType": "audio/flac", - "streamType": stream_type, - "metadata": metadata, - "duration": media.duration, - } - - async def update_flow_metadata(self, castplayer: CastPlayer) -> None: - """Update the metadata of a cast player running the flow stream.""" - if not castplayer.player.powered: - castplayer.player.poll_interval = 300 - return - if not castplayer.cc.media_controller.status.player_is_playing: - return - if castplayer.active_group: - return - if castplayer.player.state != PlayerState.PLAYING: - return - if castplayer.player.announcement_in_progress: - return - if not (queue := self.mass.player_queues.get_active_queue(castplayer.player_id)): - return - if not (current_item := queue.current_item): - return - if not (queue.flow_mode or current_item.media_type == MediaType.RADIO): - return - castplayer.player.poll_interval = 10 - media_controller = castplayer.cc.media_controller - # update metadata of current item chromecast - if media_controller.status.media_custom_data["queue_item_id"] != current_item.queue_item_id: - image_url = ( - self.mass.metadata.get_image_url(current_item.image) - if current_item.image - else MASS_LOGO_ONLINE - ) - if (streamdetails := current_item.streamdetails) and streamdetails.stream_title: - album = current_item.media_item.name - if " - " in streamdetails.stream_title: - artist, title = streamdetails.stream_title.split(" - ", 1) - else: - artist = "" - title = streamdetails.stream_title - elif media_item := current_item.media_item: - album = _album.name if (_album := getattr(media_item, "album", None)) else "" - artist = getattr(media_item, "artist_str", "") - title = media_item.name - else: - album = "" - artist = "" - title = current_item.name - flow_meta_checksum = title + image_url - if castplayer.flow_meta_checksum == flow_meta_checksum: - return - castplayer.flow_meta_checksum = flow_meta_checksum - queuedata = { - "type": "PLAY", - "mediaSessionId": media_controller.status.media_session_id, - "customData": { - "metadata": { - "metadataType": 3, - "albumName": album, - "songName": title, - "artist": artist, - "title": title, - "images": [{"url": image_url}], - } - }, - } - self.mass.create_task( - media_controller.send_message, data=queuedata, inc_session_id=True - ) - - if len(getattr(media_controller.status, "items", [])) < 2: - # In flow mode, all queue tracks are sent to the player as continuous stream. - # add a special 'command' item to the queue - # this allows for on-player next buttons/commands to still work - cmd_next_url = self.mass.streams.get_command_url(queue.queue_id, "next") - msg = { - "type": "QUEUE_INSERT", - "mediaSessionId": media_controller.status.media_session_id, - "items": [ - { - "media": { - "contentId": cmd_next_url, - "customData": { - "uri": cmd_next_url, - "queue_item_id": cmd_next_url, - "deviceName": "Music Assistant", - }, - "contentType": "audio/flac", - "streamType": STREAM_TYPE_LIVE, - "metadata": {}, - }, - "autoplay": True, - "startTime": 0, - "preloadTime": 0, - } - ], - } - self.mass.create_task(media_controller.send_message, data=msg, inc_session_id=True) diff --git a/music_assistant/server/providers/chromecast/helpers.py b/music_assistant/server/providers/chromecast/helpers.py deleted file mode 100644 index 098062c2..00000000 --- a/music_assistant/server/providers/chromecast/helpers.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Helpers to deal with Cast devices.""" - -from __future__ import annotations - -import urllib.error -from dataclasses import asdict, dataclass -from typing import TYPE_CHECKING, Self -from uuid import UUID - -from pychromecast import dial -from pychromecast.const import CAST_TYPE_GROUP - -from music_assistant.constants import VERBOSE_LOG_LEVEL - -if TYPE_CHECKING: - from pychromecast.controllers.media import MediaStatus - from pychromecast.controllers.multizone import MultizoneManager - from pychromecast.controllers.receiver import CastStatus - from pychromecast.models import CastInfo - from pychromecast.socket_client import ConnectionStatus - from zeroconf import ServiceInfo, Zeroconf - - from . import CastPlayer, ChromecastProvider - -DEFAULT_PORT = 8009 - - -@dataclass -class ChromecastInfo: - """Class to hold all data about a chromecast for creating connections. - - This also has the same attributes as the mDNS fields by zeroconf. - """ - - services: set - uuid: UUID - model_name: str - friendly_name: str - host: str - port: int - cast_type: str | None = None - manufacturer: str | None = None - is_dynamic_group: bool | None = None - is_multichannel_group: bool = False # group created for e.g. stereo pair - is_multichannel_child: bool = False # speaker that is part of multichannel setup - - @property - def is_audio_group(self) -> bool: - """Return if the cast is an audio group.""" - return self.cast_type == CAST_TYPE_GROUP - - @classmethod - def from_cast_info(cls: Self, cast_info: CastInfo) -> Self: - """Instantiate ChromecastInfo from CastInfo.""" - return cls(**asdict(cast_info)) - - def update(self, cast_info: CastInfo) -> None: - """Update ChromecastInfo from CastInfo.""" - for key, value in asdict(cast_info).items(): - if not value: - continue - setattr(self, key, value) - - def fill_out_missing_chromecast_info(self, zconf: Zeroconf) -> None: - """ - Return a new ChromecastInfo object with missing attributes filled in. - - Uses blocking HTTP / HTTPS. - """ - if self.cast_type is None or self.manufacturer is None: - # Manufacturer and cast type is not available in mDNS data, - # get it over HTTP - cast_info = dial.get_cast_type( - self, - zconf=zconf, - ) - self.cast_type = cast_info.cast_type - self.manufacturer = cast_info.manufacturer - - # Fill out missing group information via HTTP API. - dynamic_groups, multichannel_groups = get_multizone_info(self.services, zconf) - self.is_dynamic_group = self.uuid in dynamic_groups - if self.uuid in multichannel_groups: - self.is_multichannel_group = True - elif multichannel_groups: - self.is_multichannel_child = True - - -def get_multizone_info(services: list[ServiceInfo], zconf: Zeroconf, timeout=30): - """Get multizone info from eureka endpoint.""" - dynamic_groups: set[str] = set() - multichannel_groups: set[str] = set() - try: - _, status = dial._get_status( - services, - zconf, - "/setup/eureka_info?params=multizone", - True, - timeout, - None, - ) - if "multizone" in status and "dynamic_groups" in status["multizone"]: - for group in status["multizone"]["dynamic_groups"]: - if udn := group.get("uuid"): - uuid = UUID(udn.replace("-", "")) - dynamic_groups.add(uuid) - - if "multizone" in status and "groups" in status["multizone"]: - for group in status["multizone"]["groups"]: - if group["multichannel_group"] and (udn := group.get("uuid")): - uuid = UUID(udn.replace("-", "")) - multichannel_groups.add(uuid) - except (urllib.error.HTTPError, urllib.error.URLError, OSError, ValueError): - pass - return (dynamic_groups, multichannel_groups) - - -class CastStatusListener: - """ - Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__( - self, - prov: ChromecastProvider, - castplayer: CastPlayer, - mz_mgr: MultizoneManager, - mz_only=False, - ) -> None: - """Initialize the status listener.""" - self.prov = prov - self.castplayer = castplayer - self._uuid = castplayer.cc.uuid - self._valid = True - self._mz_mgr = mz_mgr - - if self.castplayer.cast_info.is_audio_group: - self._mz_mgr.add_multizone(castplayer.cc) - if mz_only: - return - - castplayer.cc.register_status_listener(self) - castplayer.cc.socket_client.media_controller.register_status_listener(self) - castplayer.cc.register_connection_listener(self) - if not self.castplayer.cast_info.is_audio_group: - self._mz_mgr.register_listener(castplayer.cc.uuid, self) - - def new_cast_status(self, status: CastStatus) -> None: - """Handle updated CastStatus.""" - if not self._valid: - return - self.prov.on_new_cast_status(self.castplayer, status) - - def new_media_status(self, status: MediaStatus) -> None: - """Handle updated MediaStatus.""" - if not self._valid: - return - self.prov.on_new_media_status(self.castplayer, status) - - def new_connection_status(self, status: ConnectionStatus) -> None: - """Handle updated ConnectionStatus.""" - if not self._valid: - return - self.prov.on_new_connection_status(self.castplayer, status) - - def added_to_multizone(self, group_uuid) -> None: - """Handle the cast added to a group.""" - self.prov.logger.debug( - "%s is added to multizone: %s", self.castplayer.player.display_name, group_uuid - ) - self.new_cast_status(self.castplayer.cc.status) - - def removed_from_multizone(self, group_uuid) -> None: - """Handle the cast removed from a group.""" - if not self._valid: - return - if group_uuid == self.castplayer.player.active_source: - self.castplayer.player.active_source = None - self.prov.logger.debug( - "%s is removed from multizone: %s", self.castplayer.player.display_name, group_uuid - ) - self.new_cast_status(self.castplayer.cc.status) - - def multizone_new_cast_status(self, group_uuid, cast_status) -> None: - """Handle reception of a new CastStatus for a group.""" - if group_player := self.prov.castplayers.get(group_uuid): - if group_player.cc.media_controller.is_active: - self.castplayer.active_group = group_uuid - elif group_uuid == self.castplayer.active_group: - self.castplayer.active_group = None - - self.prov.logger.log( - VERBOSE_LOG_LEVEL, - "%s got new cast status for group: %s", - self.castplayer.player.display_name, - group_uuid, - ) - self.new_cast_status(self.castplayer.cc.status) - - def multizone_new_media_status(self, group_uuid, media_status) -> None: - """Handle reception of a new MediaStatus for a group.""" - if not self._valid: - return - self.prov.logger.log( - VERBOSE_LOG_LEVEL, - "%s got new media_status for group: %s", - self.castplayer.player.display_name, - group_uuid, - ) - self.prov.on_new_media_status(self.castplayer, media_status) - - def load_media_failed(self, queue_item_id, error_code) -> None: - """Call when media failed to load.""" - self.prov.logger.warning( - "Load media failed: %s - error code: %s", queue_item_id, error_code - ) - - def invalidate(self) -> None: - """ - Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - if self.castplayer.cast_info.is_audio_group: - self._mz_mgr.remove_multizone(self._uuid) - else: - self._mz_mgr.deregister_listener(self._uuid, self) - self._valid = False diff --git a/music_assistant/server/providers/chromecast/manifest.json b/music_assistant/server/providers/chromecast/manifest.json deleted file mode 100644 index 6bcee60c..00000000 --- a/music_assistant/server/providers/chromecast/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "player", - "domain": "chromecast", - "name": "Chromecast", - "description": "Support for Chromecast based players.", - "codeowners": [ - "@music-assistant" - ], - "requirements": [ - "PyChromecast==14.0.4" - ], - "documentation": "https://music-assistant.io/player-support/google-cast/", - "multi_instance": false, - "builtin": false, - "icon": "cast", - "mdns_discovery": [ - "_googlecast._tcp.local." - ] -} diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py deleted file mode 100644 index 93c3614c..00000000 --- a/music_assistant/server/providers/deezer/__init__.py +++ /dev/null @@ -1,769 +0,0 @@ -"""Deezer music provider support for MusicAssistant.""" - -import hashlib -import uuid -from asyncio import TaskGroup -from collections.abc import AsyncGenerator -from dataclasses import dataclass -from math import ceil - -import deezer -from aiohttp import ClientSession, ClientTimeout -from Crypto.Cipher import Blowfish -from deezer import exceptions as deezer_exceptions - -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ( - AlbumType, - ConfigEntryType, - ContentType, - ExternalID, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import LoginFailed -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - ItemMapping, - MediaItemImage, - MediaItemMetadata, - MediaItemType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models import ProviderInstanceType -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.server import MusicAssistant - -from .gw_client import GWClient - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.ALBUM_METADATA, - ProviderFeature.TRACK_METADATA, - ProviderFeature.ARTIST_METADATA, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ProviderFeature.PLAYLIST_CREATE, - ProviderFeature.RECOMMENDATIONS, - ProviderFeature.SIMILAR_TRACKS, -) - - -@dataclass -class DeezerCredentials: - """Class for storing credentials.""" - - app_id: int - app_secret: str - access_token: str - - -CONF_ACCESS_TOKEN = "access_token" -CONF_ARL_TOKEN = "arl_token" -CONF_ACTION_AUTH = "auth" -DEEZER_AUTH_URL = "https://connect.deezer.com/oauth/auth.php" -RELAY_URL = "https://deezer.oauth.jonathanbangert.com/" -DEEZER_PERMS = "basic_access,email,offline_access,manage_library,\ -manage_community,delete_library,listening_history" -DEEZER_APP_ID = app_var(6) -DEEZER_APP_SECRET = app_var(7) - - -async def get_access_token( - app_id: str, app_secret: str, code: str, http_session: ClientSession -) -> str: - """Update the access_token.""" - response = await http_session.post( - "https://connect.deezer.com/oauth/access_token.php", - params={"code": code, "app_id": app_id, "secret": app_secret}, - ssl=False, - ) - if response.status != 200: - msg = f"HTTP Error {response.status}: {response.reason}" - raise ConnectionError(msg) - response_text = await response.text() - try: - return response_text.split("=")[1].split("&")[0] - except Exception as error: - msg = "Invalid auth code" - raise LoginFailed(msg) from error - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return DeezerProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """Return Config entries to setup this provider.""" - # Action is to launch oauth flow - if action == CONF_ACTION_AUTH: - # Use the AuthenticationHelper to authenticate - async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: # type: ignore - url = f"{DEEZER_AUTH_URL}?app_id={DEEZER_APP_ID}&redirect_uri={RELAY_URL}\ -&perms={DEEZER_PERMS}&state={auth_helper.callback_url}" - code = (await auth_helper.authenticate(url))["code"] - values[CONF_ACCESS_TOKEN] = await get_access_token( # type: ignore - DEEZER_APP_ID, DEEZER_APP_SECRET, code, mass.http_session - ) - - return ( - ConfigEntry( - key=CONF_ACCESS_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Access token", - required=True, - action=CONF_ACTION_AUTH, - description="You need to authenticate on Deezer.", - action_label="Authenticate with Deezer", - value=values.get(CONF_ACCESS_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_ARL_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Arl token", - required=True, - description="See https://www.dumpmedia.com/deezplus/deezer-arl.html", - value=values.get(CONF_ARL_TOKEN) if values else None, - ), - ) - - -class DeezerProvider(MusicProvider): - """Deezer provider support.""" - - client: deezer.Client - gw_client: GWClient - credentials: DeezerCredentials - user: deezer.User - - async def handle_async_init(self) -> None: - """Handle async init of the Deezer provider.""" - self.credentials = DeezerCredentials( - app_id=DEEZER_APP_ID, - app_secret=DEEZER_APP_SECRET, - access_token=self.config.get_value(CONF_ACCESS_TOKEN), # type: ignore - ) - - self.client = deezer.Client( - app_id=self.credentials.app_id, - app_secret=self.credentials.app_secret, - access_token=self.credentials.access_token, - ) - - self.user = await self.client.get_user() - - self.gw_client = GWClient( - self.mass.http_session, - self.config.get_value(CONF_ACCESS_TOKEN), - self.config.get_value(CONF_ARL_TOKEN), - ) - await self.gw_client.setup() - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def search( - self, search_query: str, media_types=list[MediaType], limit: int = 5 - ) -> SearchResults: - """Perform search on music provider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - """ - # Create a task for each media_type - tasks = {} - - async with TaskGroup() as taskgroup: - for media_type in media_types: - if media_type == MediaType.TRACK: - tasks[MediaType.TRACK] = taskgroup.create_task( - self.search_and_parse_tracks( - query=search_query, - limit=limit, - user_country=self.gw_client.user_country, - ) - ) - elif media_type == MediaType.ARTIST: - tasks[MediaType.ARTIST] = taskgroup.create_task( - self.search_and_parse_artists(query=search_query, limit=limit) - ) - elif media_type == MediaType.ALBUM: - tasks[MediaType.ALBUM] = taskgroup.create_task( - self.search_and_parse_albums(query=search_query, limit=limit) - ) - elif media_type == MediaType.PLAYLIST: - tasks[MediaType.PLAYLIST] = taskgroup.create_task( - self.search_and_parse_playlists(query=search_query, limit=limit) - ) - - results = SearchResults() - - for media_type, task in tasks.items(): - if media_type == MediaType.ARTIST: - results.artists = task.result() - elif media_type == MediaType.ALBUM: - results.albums = task.result() - elif media_type == MediaType.TRACK: - results.tracks = task.result() - elif media_type == MediaType.PLAYLIST: - results.playlists = task.result() - - return results - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Deezer.""" - async for artist in await self.client.get_user_artists(): - yield self.parse_artist(artist=artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Deezer.""" - async for album in await self.client.get_user_albums(): - yield self.parse_album(album=album) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from Deezer.""" - async for playlist in await self.user.get_playlists(): - yield self.parse_playlist(playlist=playlist) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve all library tracks from Deezer.""" - async for track in await self.client.get_user_tracks(): - yield self.parse_track(track=track, user_country=self.gw_client.user_country) - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - try: - return self.parse_artist( - artist=await self.client.get_artist(artist_id=int(prov_artist_id)) - ) - except deezer_exceptions.DeezerErrorResponse as error: - self.logger.warning("Failed getting artist: %s", error) - - async def get_album(self, prov_album_id: str) -> Album: - """Get full album details by id.""" - try: - return self.parse_album(album=await self.client.get_album(album_id=int(prov_album_id))) - except deezer_exceptions.DeezerErrorResponse as error: - self.logger.warning("Failed getting album: %s", error) - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id.""" - try: - return self.parse_playlist( - playlist=await self.client.get_playlist(playlist_id=int(prov_playlist_id)), - ) - except deezer_exceptions.DeezerErrorResponse as error: - self.logger.warning("Failed getting playlist: %s", error) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - try: - return self.parse_track( - track=await self.client.get_track(track_id=int(prov_track_id)), - user_country=self.gw_client.user_country, - ) - except deezer_exceptions.DeezerErrorResponse as error: - self.logger.warning("Failed getting track: %s", error) - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get all tracks in an album.""" - album = await self.client.get_album(album_id=int(prov_album_id)) - return [ - self.parse_track( - track=deezer_track, - user_country=self.gw_client.user_country, - # TODO: doesn't Deezer have disc and track number in the api ? - position=0, - ) - for deezer_track in await album.get_tracks() - ] - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # paging not supported, we always return the whole list at once - return [] - # TODO: access the underlying paging on the deezer api (if possible)) - playlist = await self.client.get_playlist(int(prov_playlist_id)) - playlist_tracks = await playlist.get_tracks() - for index, deezer_track in enumerate(playlist_tracks, 1): - result.append( - self.parse_track( - track=deezer_track, - user_country=self.gw_client.user_country, - position=index, - ) - ) - return result - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get albums by an artist.""" - artist = await self.client.get_artist(artist_id=int(prov_artist_id)) - return [self.parse_album(album=album) async for album in await artist.get_albums()] - - async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: - """Get top 50 tracks of an artist.""" - artist = await self.client.get_artist(artist_id=int(prov_artist_id)) - return [ - self.parse_track(track=track, user_country=self.gw_client.user_country) - async for track in await artist.get_top(limit=50) - ] - - async def library_add(self, item: MediaItemType) -> bool: - """Add an item to the provider's library/favorites.""" - result = False - if item.media_type == MediaType.ARTIST: - result = await self.client.add_user_artist( - artist_id=int(item.item_id), - ) - elif item.media_type == MediaType.ALBUM: - result = await self.client.add_user_album( - album_id=int(item.item_id), - ) - elif item.media_type == MediaType.TRACK: - result = await self.client.add_user_track( - track_id=int(item.item_id), - ) - elif item.media_type == MediaType.PLAYLIST: - result = await self.client.add_user_playlist( - playlist_id=int(item.item_id), - ) - else: - raise NotImplementedError - return result - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove an item from the provider's library/favorites.""" - result = False - if media_type == MediaType.ARTIST: - result = await self.client.remove_user_artist( - artist_id=int(prov_item_id), - ) - elif media_type == MediaType.ALBUM: - result = await self.client.remove_user_album( - album_id=int(prov_item_id), - ) - elif media_type == MediaType.TRACK: - result = await self.client.remove_user_track( - track_id=int(prov_item_id), - ) - elif media_type == MediaType.PLAYLIST: - result = await self.client.remove_user_playlist( - playlist_id=int(prov_item_id), - ) - else: - raise NotImplementedError - return result - - async def recommendations(self) -> list[Track]: - """Get deezer's recommendations.""" - return [ - self.parse_track(track=track, user_country=self.gw_client.user_country) - for track in await self.client.get_user_recommended_tracks() - ] - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - playlist = await self.client.get_playlist(int(prov_playlist_id)) - await playlist.add_tracks(tracks=[int(i) for i in prov_track_ids]) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - playlist_track_ids = [] - for track in await self.get_playlist_tracks(prov_playlist_id, 0): - if track.position in positions_to_remove: - playlist_track_ids.append(int(track.item_id)) - if len(playlist_track_ids) == len(positions_to_remove): - break - playlist = await self.client.get_playlist(int(prov_playlist_id)) - await playlist.delete_tracks(playlist_track_ids) - - async def create_playlist(self, name: str) -> Playlist: - """Create a new playlist on provider with given name.""" - playlist_id = await self.client.create_playlist(playlist_name=name) - playlist = await self.client.get_playlist(playlist_id) - return self.parse_playlist(playlist=playlist) - - async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - endpoint = "song.getSearchTrackMix" - tracks = (await self.gw_client._gw_api_call(endpoint, args={"SNG_ID": prov_track_id}))[ - "results" - ]["data"][:limit] - return [await self.get_track(track["SNG_ID"]) for track in tracks] - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - url_details, song_data = await self.gw_client.get_deezer_track_urls(item_id) - url = url_details["sources"][0]["url"] - return StreamDetails( - item_id=item_id, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(url_details["format"].split("_")[0]) - ), - stream_type=StreamType.CUSTOM, - duration=int(song_data["DURATION"]), - data={"url": url, "format": url_details["format"]}, - size=int(song_data[f"FILESIZE_{url_details['format']}"]), - ) - - async def get_audio_stream( - self, streamdetails: StreamDetails, seek_position: int = 0 - ) -> AsyncGenerator[bytes, None]: - """Return the audio stream for the provider item.""" - blowfish_key = self.get_blowfish_key(streamdetails.item_id) - chunk_index = 0 - timeout = ClientTimeout(total=0, connect=30, sock_read=600) - headers = {} - # if seek_position and streamdetails.size: - # chunk_count = ceil(streamdetails.size / 2048) - # chunk_index = int(chunk_count / streamdetails.duration) * seek_position - # skip_bytes = chunk_index * 2048 - # headers["Range"] = f"bytes={skip_bytes}-" - - # NOTE: Seek with using the Range header is not working properly - # causing malformed audio so this is a temporary patch - # by just skipping chunks - if seek_position and streamdetails.size: - chunk_count = ceil(streamdetails.size / 2048) - skip_chunks = int(chunk_count / streamdetails.duration) * seek_position - else: - skip_chunks = 0 - - buffer = bytearray() - streamdetails.data["start_ts"] = utc_timestamp() - streamdetails.data["stream_id"] = uuid.uuid1() - self.mass.create_task(self.gw_client.log_listen(next_track=streamdetails.item_id)) - async with self.mass.http_session.get( - streamdetails.data["url"], headers=headers, timeout=timeout - ) as resp: - async for chunk in resp.content.iter_chunked(2048): - buffer += chunk - if len(buffer) >= 2048: - if chunk_index >= skip_chunks or chunk_index == 0: - if chunk_index % 3 > 0: - yield bytes(buffer[:2048]) - else: - yield self.decrypt_chunk(bytes(buffer[:2048]), blowfish_key) - - chunk_index += 1 - del buffer[:2048] - yield bytes(buffer) - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - await self.gw_client.log_listen(last_track=streamdetails) - - ### PARSING METADATA FUNCTIONS ### - - def parse_metadata_track(self, track: deezer.Track) -> MediaItemMetadata: - """Parse the track metadata.""" - metadata = MediaItemMetadata() - if hasattr(track, "preview"): - metadata.preview = track.preview - if hasattr(track, "explicit_lyrics"): - metadata.explicit = track.explicit_lyrics - if hasattr(track, "duration"): - metadata.duration = track.duration - if hasattr(track, "rank"): - metadata.popularity = track.rank - if hasattr(track, "release_date"): - metadata.release_date = track.release_date - if hasattr(track, "album") and hasattr(track.album, "cover_big"): - metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=track.album.cover_big, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - return metadata - - def parse_metadata_album(self, album: deezer.Album) -> MediaItemMetadata: - """Parse the album metadata.""" - return MediaItemMetadata( - explicit=album.explicit_lyrics, - images=[ - MediaItemImage( - type=ImageType.THUMB, - path=album.cover_big, - provider=self.lookup_key, - remotely_accessible=True, - ) - ], - ) - - def parse_metadata_artist(self, artist: deezer.Artist) -> MediaItemMetadata: - """Parse the artist metadata.""" - return MediaItemMetadata( - images=[ - MediaItemImage( - type=ImageType.THUMB, - path=artist.picture_big, - provider=self.lookup_key, - remotely_accessible=True, - ) - ], - ) - - ### PARSING FUNCTIONS ### - def parse_artist(self, artist: deezer.Artist) -> Artist: - """Parse the deezer-python artist to a Music Assistant artist.""" - return Artist( - item_id=str(artist.id), - provider=self.domain, - name=artist.name, - media_type=MediaType.ARTIST, - provider_mappings={ - ProviderMapping( - item_id=str(artist.id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=artist.link, - ) - }, - metadata=self.parse_metadata_artist(artist=artist), - ) - - def parse_album(self, album: deezer.Album) -> Album: - """Parse the deezer-python album to a Music Assistant album.""" - return Album( - album_type=AlbumType(album.type), - item_id=str(album.id), - provider=self.domain, - name=album.title, - artists=[ - ItemMapping( - media_type=MediaType.ARTIST, - item_id=str(album.artist.id), - provider=self.instance_id, - name=album.artist.name, - ) - ], - media_type=MediaType.ALBUM, - provider_mappings={ - ProviderMapping( - item_id=str(album.id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=album.link, - ) - }, - metadata=self.parse_metadata_album(album=album), - ) - - def parse_playlist(self, playlist: deezer.Playlist) -> Playlist: - """Parse the deezer-python playlist to a Music Assistant playlist.""" - creator = self.get_playlist_creator(playlist) - return Playlist( - item_id=str(playlist.id), - provider=self.domain, - name=playlist.title, - media_type=MediaType.PLAYLIST, - provider_mappings={ - ProviderMapping( - item_id=str(playlist.id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=playlist.link, - ) - }, - metadata=MediaItemMetadata( - images=[ - MediaItemImage( - type=ImageType.THUMB, - path=playlist.picture_big, - provider=self.lookup_key, - remotely_accessible=True, - ) - ], - ), - is_editable=creator.id == self.user.id, - owner=creator.name, - cache_checksum=playlist.checksum, - ) - - def get_playlist_creator(self, playlist: deezer.Playlist): - """On playlists, the creator is called creator, elsewhere it's called user.""" - if hasattr(playlist, "creator"): - return playlist.creator - return playlist.user - - def parse_track(self, track: deezer.Track, user_country: str, position: int = 0) -> Track: - """Parse the deezer-python track to a Music Assistant track.""" - if hasattr(track, "artist"): - artist = ItemMapping( - media_type=MediaType.ARTIST, - item_id=str(getattr(track.artist, "id", f"deezer-{track.artist.name}")), - provider=self.instance_id, - name=track.artist.name, - ) - else: - artist = None - if hasattr(track, "album"): - album = ItemMapping( - media_type=MediaType.ALBUM, - item_id=str(track.album.id), - provider=self.instance_id, - name=track.album.title, - ) - else: - album = None - - item = Track( - item_id=str(track.id), - provider=self.domain, - name=track.title, - sort_name=self.get_short_title(track), - duration=track.duration, - artists=[artist] if artist else [], - album=album, - provider_mappings={ - ProviderMapping( - item_id=str(track.id), - provider_domain=self.domain, - provider_instance=self.instance_id, - available=self.track_available(track=track, user_country=user_country), - url=track.link, - ) - }, - metadata=self.parse_metadata_track(track=track), - track_number=position, - position=position, - disc_number=getattr(track, "disk_number", 0), - ) - if isrc := getattr(track, "isrc", None): - item.external_ids.add((ExternalID.ISRC, isrc)) - return item - - def get_short_title(self, track: deezer.Track): - """Short names only returned, if available.""" - if hasattr(track, "title_short"): - return track.title_short - return track.title - - ### SEARCH AND PARSE FUNCTIONS ### - async def search_and_parse_tracks( - self, query: str, user_country: str, limit: int = 20 - ) -> list[Track]: - """Search for tracks and parse them.""" - deezer_tracks = await self.client.search(query=query, limit=limit) - tracks = [] - for index, track in enumerate(deezer_tracks): - tracks.append(self.parse_track(track, user_country)) - if index == limit: - return tracks - return tracks - - async def search_and_parse_artists(self, query: str, limit: int = 20) -> list[Artist]: - """Search for artists and parse them.""" - deezer_artist = await self.client.search_artists(query=query, limit=limit) - artists = [] - for index, artist in enumerate(deezer_artist): - artists.append(self.parse_artist(artist)) - if index == limit: - return artists - return artists - - async def search_and_parse_albums(self, query: str, limit: int = 20) -> list[Album]: - """Search for album and parse them.""" - deezer_albums = await self.client.search_albums(query=query, limit=limit) - albums = [] - for index, album in enumerate(deezer_albums): - albums.append(self.parse_album(album)) - if index == limit: - return albums - return albums - - async def search_and_parse_playlists(self, query: str, limit: int = 20) -> list[Playlist]: - """Search for playlists and parse them.""" - deezer_playlists = await self.client.search_playlists(query=query, limit=limit) - playlists = [] - for index, playlist in enumerate(deezer_playlists): - playlists.append(self.parse_playlist(playlist)) - if index == limit: - return playlists - return playlists - - ### OTHER FUNCTIONS ### - - async def get_track_content_type(self, gw_client: GWClient, track_id: int): - """Get a tracks contentType.""" - song_data = await gw_client.get_song_data(track_id) - if song_data["results"]["FILESIZE_FLAC"]: - return ContentType.FLAC - - if song_data["results"]["FILESIZE_MP3_320"] or song_data["results"]["FILESIZE_MP3_128"]: - return ContentType.MP3 - - msg = "Unsupported contenttype" - raise NotImplementedError(msg) - - def track_available(self, track: deezer.Track, user_country: str) -> bool: - """Check if a given track is available in the users country.""" - if hasattr(track, "available_countries"): - return user_country in track.available_countries - return True - - def _md5(self, data, data_type="ascii"): - md5sum = hashlib.md5() - md5sum.update(data.encode(data_type)) - return md5sum.hexdigest() - - def get_blowfish_key(self, track_id): - """Get blowfish key to decrypt a chunk of a track.""" - secret = app_var(5) - id_md5 = self._md5(track_id) - return "".join( - chr(ord(id_md5[i]) ^ ord(id_md5[i + 16]) ^ ord(secret[i])) for i in range(16) - ) - - def decrypt_chunk(self, chunk, blowfish_key): - """Decrypt a given chunk using the blow fish key.""" - cipher = Blowfish.new( - blowfish_key.encode("ascii"), - Blowfish.MODE_CBC, - b"\x00\x01\x02\x03\x04\x05\x06\x07", - ) - return cipher.decrypt(chunk) diff --git a/music_assistant/server/providers/deezer/gw_client.py b/music_assistant/server/providers/deezer/gw_client.py deleted file mode 100644 index 0a03c298..00000000 --- a/music_assistant/server/providers/deezer/gw_client.py +++ /dev/null @@ -1,197 +0,0 @@ -"""A minimal client for the unofficial gw-API, which deezer is using on their website and app. - -Credits go out to RemixDev (https://gitlab.com/RemixDev) for figuring out, how to get the arl -cookie based on the api_token. -""" - -import datetime -from http.cookies import BaseCookie, Morsel - -from aiohttp import ClientSession -from yarl import URL - -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.models.streamdetails import StreamDetails - -USER_AGENT_HEADER = ( - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/79.0.3945.130 Safari/537.36" -) - -GW_LIGHT_URL = "https://www.deezer.com/ajax/gw-light.php" - - -class DeezerGWError(BaseException): - """Exception type for GWClient related exceptions.""" - - -class GWClient: - """The GWClient class can be used to perform actions not being of the official API.""" - - _arl_token: str - _api_token: str - _gw_csrf_token: str | None - _license: str | None - _license_expiration_timestamp: int - session: ClientSession - formats: list[dict[str, str]] = [ - {"cipher": "BF_CBC_STRIPE", "format": "MP3_128"}, - ] - user_country: str - - def __init__(self, session: ClientSession, api_token: str, arl_token: str) -> None: - """Provide an aiohttp ClientSession and the deezer api_token.""" - self._api_token = api_token - self._arl_token = arl_token - self.session = session - - async def _set_cookie(self) -> None: - cookie = Morsel() - - cookie.set("arl", self._arl_token, self._arl_token) - cookie.domain = ".deezer.com" - cookie.path = "/" - cookie.httponly = {"HttpOnly": True} - - self.session.cookie_jar.update_cookies(BaseCookie({"arl": cookie}), URL(GW_LIGHT_URL)) - - async def _update_user_data(self) -> None: - user_data = await self._gw_api_call("deezer.getUserData", False) - if not user_data["results"]["USER"]["USER_ID"]: - await self._set_cookie() - user_data = await self._gw_api_call("deezer.getUserData", False) - - if not user_data["results"]["OFFER_ID"]: - msg = "Free subscriptions cannot be used in MA. Make sure you set a valid ARL." - raise DeezerGWError(msg) - - self._gw_csrf_token = user_data["results"]["checkForm"] - self._license = user_data["results"]["USER"]["OPTIONS"]["license_token"] - self._license_expiration_timestamp = user_data["results"]["USER"]["OPTIONS"][ - "expiration_timestamp" - ] - web_qualities = user_data["results"]["USER"]["OPTIONS"]["web_sound_quality"] - mobile_qualities = user_data["results"]["USER"]["OPTIONS"]["mobile_sound_quality"] - if web_qualities["high"] or mobile_qualities["high"]: - self.formats.insert(0, {"cipher": "BF_CBC_STRIPE", "format": "MP3_320"}) - if web_qualities["lossless"] or mobile_qualities["lossless"]: - self.formats.insert(0, {"cipher": "BF_CBC_STRIPE", "format": "FLAC"}) - - self.user_country = user_data["results"]["COUNTRY"] - - async def setup(self) -> None: - """Call this to let the client get its cookies, license and tokens.""" - await self._set_cookie() - await self._update_user_data() - - async def _get_license(self): - if ( - self._license_expiration_timestamp - < (datetime.datetime.now() + datetime.timedelta(days=1)).timestamp() - ): - await self._update_user_data() - return self._license - - async def _gw_api_call( - self, method, use_csrf_token=True, args=None, params=None, http_method="POST", retry=True - ): - csrf_token = self._gw_csrf_token if use_csrf_token else "null" - if params is None: - params = {} - parameters = {"api_version": "1.0", "api_token": csrf_token, "input": "3", "method": method} - parameters |= params - result = await self.session.request( - http_method, - GW_LIGHT_URL, - params=parameters, - timeout=30, - json=args, - headers={"User-Agent": USER_AGENT_HEADER}, - ) - result_json = await result.json() - - if result_json["error"]: - if retry: - await self._update_user_data() - return await self._gw_api_call( - method, use_csrf_token, args, params, http_method, False - ) - else: - msg = "Failed to call GW-API" - raise DeezerGWError(msg, result_json["error"]) - return result_json - - async def get_song_data(self, track_id): - """Get data such as the track token for a given track.""" - return await self._gw_api_call("song.getData", args={"SNG_ID": track_id}) - - async def get_deezer_track_urls(self, track_id): - """Get the URL for a given track id.""" - dz_license = await self._get_license() - song_data = await self.get_song_data(track_id) - track_token = song_data["results"]["TRACK_TOKEN"] - url_data = { - "license_token": dz_license, - "media": [ - { - "type": "FULL", - "formats": self.formats, - } - ], - "track_tokens": [track_token], - } - url_response = await self.session.post( - "https://media.deezer.com/v1/get_url", - json=url_data, - headers={"User-Agent": USER_AGENT_HEADER}, - ) - result_json = await url_response.json() - - if error := result_json["data"][0].get("errors"): - msg = "Received an error from API" - raise DeezerGWError(msg, error) - - return result_json["data"][0]["media"][0], song_data["results"] - - async def log_listen( - self, next_track: str | None = None, last_track: StreamDetails | None = None - ) -> None: - """Log the next and/or previous track of the current playback queue.""" - if not (next_track or last_track): - msg = "last or current track information must be provided." - raise DeezerGWError(msg) - - payload = {} - - if next_track: - payload["next_media"] = {"media": {"id": next_track, "type": "song"}} - - if last_track: - seconds_streamed = min( - utc_timestamp() - last_track.data["start_ts"], - last_track.seconds_streamed, - ) - - payload["params"] = { - "media": { - "id": last_track.item_id, - "type": "song", - "format": last_track.data["format"], - }, - "type": 1, - "stat": { - "seek": 1 if seconds_streamed < last_track.duration else 0, - "pause": 0, - "sync": 0, - "next": bool(next_track), - }, - "lt": int(seconds_streamed), - "ctxt": {"t": "search_page", "id": last_track.item_id}, - "dev": {"v": "10020230525142740", "t": 0}, - "ls": [], - "ts_listen": int(last_track.data["start_ts"]), - "is_shuffle": False, - "stream_id": str(last_track.data["stream_id"]), - } - - await self._gw_api_call("log.listen", args=payload) diff --git a/music_assistant/server/providers/deezer/icon.svg b/music_assistant/server/providers/deezer/icon.svg deleted file mode 100644 index 1c6170d3..00000000 --- a/music_assistant/server/providers/deezer/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/music_assistant/server/providers/deezer/manifest.json b/music_assistant/server/providers/deezer/manifest.json deleted file mode 100644 index 0324e93e..00000000 --- a/music_assistant/server/providers/deezer/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "deezer", - "name": "Deezer", - "description": "Support for the Deezer streaming provider in Music Assistant.", - "codeowners": ["@arctixdev", "@micha91"], - "documentation": "https://music-assistant.io/music-providers/deezer/", - "requirements": ["deezer-python-async==0.3.0", "pycryptodome==3.21.0"], - "multi_instance": true -} diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py deleted file mode 100644 index fadc3dff..00000000 --- a/music_assistant/server/providers/dlna/__init__.py +++ /dev/null @@ -1,618 +0,0 @@ -"""DLNA/uPNP Player provider for Music Assistant. - -Most of this code is based on the implementation within Home Assistant: -https://github.com/home-assistant/core/blob/dev/homeassistant/components/dlna_dmr - -All rights/credits reserved. -""" - -from __future__ import annotations - -import asyncio -import functools -import logging -import time -from contextlib import suppress -from dataclasses import dataclass, field -from ipaddress import IPv4Address -from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar - -from async_upnp_client.aiohttp import AiohttpSessionRequester -from async_upnp_client.client_factory import UpnpFactory -from async_upnp_client.exceptions import UpnpError, UpnpResponseError -from async_upnp_client.profiles.dlna import DmrDevice, TransportState -from async_upnp_client.search import async_search - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_ENABLE_ICY_METADATA, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, - CONF_ENTRY_HTTP_PROFILE, - ConfigEntry, - ConfigValueType, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import CONF_ENFORCE_MP3, CONF_PLAYERS, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.didl_lite import create_didl_metadata -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider - -from .helpers import DLNANotifyServer - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Coroutine, Sequence - - from async_upnp_client.client import UpnpRequester, UpnpService, UpnpStateVariable - from async_upnp_client.utils import CaseInsensitiveDict - - from music_assistant.common.models.config_entries import PlayerConfig, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -PLAYER_CONFIG_ENTRIES = ( - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_HTTP_PROFILE, - CONF_ENTRY_ENABLE_ICY_METADATA, - # enable flow mode by default because - # most dlna players do not support enqueueing - CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, - create_sample_rates_config_entry(192000, 24, 96000, 24), -) - - -CONF_NETWORK_SCAN = "network_scan" - -_DLNAPlayerProviderT = TypeVar("_DLNAPlayerProviderT", bound="DLNAPlayerProvider") -_R = TypeVar("_R") -_P = ParamSpec("_P") - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return DLNAPlayerProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_NETWORK_SCAN, - type=ConfigEntryType.BOOLEAN, - label="Allow network scan for discovery", - default_value=False, - description="Enable network scan for discovery of players. \n" - "Can be used if (some of) your players are not automatically discovered.", - ), - ) - - -def catch_request_errors( - func: Callable[Concatenate[_DLNAPlayerProviderT, _P], Awaitable[_R]], -) -> Callable[Concatenate[_DLNAPlayerProviderT, _P], Coroutine[Any, Any, _R | None]]: - """Catch UpnpError errors.""" - - @functools.wraps(func) - async def wrapper(self: _DLNAPlayerProviderT, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: - """Catch UpnpError errors and check availability before and after request.""" - player_id = kwargs["player_id"] if "player_id" in kwargs else args[0] - dlna_player = self.dlnaplayers[player_id] - dlna_player.last_command = time.time() - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - self.logger.debug( - "Handling command %s for player %s", - func.__name__, - dlna_player.player.display_name, - ) - if not dlna_player.available: - self.logger.warning("Device disappeared when trying to call %s", func.__name__) - return None - try: - return await func(self, *args, **kwargs) - except UpnpError as err: - dlna_player.force_poll = True - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - self.logger.exception("Error during call %s: %r", func.__name__, err) - else: - self.logger.error("Error during call %s: %r", func.__name__, str(err)) - return None - - return wrapper - - -@dataclass -class DLNAPlayer: - """Class that holds all dlna variables for a player.""" - - udn: str # = player_id - player: Player # mass player - description_url: str # last known location (description.xml) url - - device: DmrDevice | None = None - lock: asyncio.Lock = field( - default_factory=asyncio.Lock - ) # Held when connecting or disconnecting the device - force_poll: bool = False - ssdp_connect_failed: bool = False - - # Track BOOTID in SSDP advertisements for device changes - bootid: int | None = None - last_seen: float = field(default_factory=time.time) - last_command: float = field(default_factory=time.time) - - def update_attributes(self) -> None: - """Update attributes of the MA Player from DLNA state.""" - # generic attributes - - if self.available: - self.player.available = True - self.player.name = self.device.name - self.player.volume_level = int((self.device.volume_level or 0) * 100) - self.player.volume_muted = self.device.is_volume_muted or False - self.player.state = self.get_state(self.device) - self.player.current_item_id = self.device.current_track_uri or "" - if self.player.player_id in self.player.current_item_id: - self.player.active_source = self.player.player_id - elif "spotify" in self.player.current_item_id: - self.player.active_source = "spotify" - elif self.player.current_item_id.startswith("http"): - self.player.active_source = "http" - else: - # TODO: handle other possible sources here - self.player.active_source = None - if self.device.media_position: - # only update elapsed_time if the device actually reports it - self.player.elapsed_time = float(self.device.media_position) - if self.device.media_position_updated_at is not None: - self.player.elapsed_time_last_updated = ( - self.device.media_position_updated_at.timestamp() - ) - else: - # device is unavailable - self.player.available = False - - @property - def available(self) -> bool: - """Device is available when we have a connection to it.""" - return self.device is not None and self.device.profile_device.available - - @staticmethod - def get_state(device: DmrDevice) -> PlayerState: - """Return current PlayerState of the player.""" - if device.transport_state is None: - return PlayerState.IDLE - if device.transport_state in ( - TransportState.PLAYING, - TransportState.TRANSITIONING, - ): - return PlayerState.PLAYING - if device.transport_state in ( - TransportState.PAUSED_PLAYBACK, - TransportState.PAUSED_RECORDING, - ): - return PlayerState.PAUSED - if device.transport_state == TransportState.VENDOR_DEFINED: - # Unable to map this state to anything reasonable, fallback to idle - return PlayerState.IDLE - - return PlayerState.IDLE - - -class DLNAPlayerProvider(PlayerProvider): - """DLNA Player provider.""" - - dlnaplayers: dict[str, DLNAPlayer] | None = None - _discovery_running: bool = False - - lock: asyncio.Lock - requester: UpnpRequester - upnp_factory: UpnpFactory - notify_server: DLNANotifyServer - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.dlnaplayers = {} - self.lock = asyncio.Lock() - # silence the async_upnp_client logger - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("async_upnp_client").setLevel(logging.DEBUG) - else: - logging.getLogger("async_upnp_client").setLevel(self.logger.level + 10) - self.requester = AiohttpSessionRequester(self.mass.http_session, with_sleep=True) - self.upnp_factory = UpnpFactory(self.requester, non_strict=True) - self.notify_server = DLNANotifyServer(self.requester, self.mass) - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - self.mass.streams.unregister_dynamic_route("/notify", "NOTIFY") - async with TaskManager(self.mass) as tg: - for dlna_player in self.dlnaplayers.values(): - tg.create_task(self._device_disconnect(dlna_player)) - - async def get_player_config_entries( - self, - player_id: str, - ) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - base_entries = await super().get_player_config_entries(player_id) - return base_entries + PLAYER_CONFIG_ENTRIES - - async def on_player_config_change( - self, - config: PlayerConfig, - changed_keys: set[str], - ) -> None: - """Call (by config manager) when the configuration of a player changes.""" - if dlna_player := self.dlnaplayers.get(config.player_id): - # reset player features based on config values - self._set_player_features(dlna_player) - else: - # run discovery to catch any re-enabled players - self.mass.create_task(self.discover_players()) - - @catch_request_errors - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - dlna_player = self.dlnaplayers[player_id] - assert dlna_player.device is not None - await dlna_player.device.async_stop() - - @catch_request_errors - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - dlna_player = self.dlnaplayers[player_id] - assert dlna_player.device is not None - await dlna_player.device.async_play() - - @catch_request_errors - async def play_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle PLAY MEDIA on given player.""" - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): - media.uri = media.uri.replace(".flac", ".mp3") - dlna_player = self.dlnaplayers[player_id] - # always clear queue (by sending stop) first - if dlna_player.device.can_stop: - await self.cmd_stop(player_id) - didl_metadata = create_didl_metadata(media) - title = media.title or media.uri - await dlna_player.device.async_set_transport_uri(media.uri, title, didl_metadata) - # Play it - await dlna_player.device.async_wait_for_can_play(10) - # optimistically set this timestamp to help in case of a player - # that does not report the progress - now = time.time() - dlna_player.player.elapsed_time = 0 - dlna_player.player.elapsed_time_last_updated = now - await dlna_player.device.async_play() - # force poll the device - for sleep in (1, 2): - await asyncio.sleep(sleep) - dlna_player.force_poll = True - await self.poll_player(dlna_player.udn) - - @catch_request_errors - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next queue item on the player.""" - dlna_player = self.dlnaplayers[player_id] - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False): - media.uri = media.uri.replace(".flac", ".mp3") - didl_metadata = create_didl_metadata(media) - title = media.title or media.uri - try: - await dlna_player.device.async_set_next_transport_uri(media.uri, title, didl_metadata) - except UpnpError: - self.logger.error( - "Enqueuing the next track failed for player %s - " - "the player probably doesn't support this. " - "Enable 'flow mode' for this player.", - dlna_player.player.display_name, - ) - else: - self.logger.debug( - "Enqued next track (%s) to player %s", - title, - dlna_player.player.display_name, - ) - - @catch_request_errors - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - dlna_player = self.dlnaplayers[player_id] - assert dlna_player.device is not None - if dlna_player.device.can_pause: - await dlna_player.device.async_pause() - else: - await dlna_player.device.async_stop() - - @catch_request_errors - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - dlna_player = self.dlnaplayers[player_id] - assert dlna_player.device is not None - await dlna_player.device.async_set_volume_level(volume_level / 100) - - @catch_request_errors - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - dlna_player = self.dlnaplayers[player_id] - assert dlna_player.device is not None - await dlna_player.device.async_mute_volume(muted) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - dlna_player = self.dlnaplayers[player_id] - - # try to reconnect the device if the connection was lost - if not dlna_player.device: - if not dlna_player.force_poll: - return - try: - await self._device_connect(dlna_player) - except UpnpError as err: - raise PlayerUnavailableError from err - - assert dlna_player.device is not None - - try: - now = time.time() - do_ping = dlna_player.force_poll or (now - dlna_player.last_seen) > 60 - with suppress(ValueError): - await dlna_player.device.async_update(do_ping=do_ping) - dlna_player.last_seen = now if do_ping else dlna_player.last_seen - except UpnpError as err: - self.logger.debug("Device unavailable: %r", err) - await self._device_disconnect(dlna_player) - raise PlayerUnavailableError from err - finally: - dlna_player.force_poll = False - - async def discover_players(self, use_multicast: bool = False) -> None: - """Discover DLNA players on the network.""" - if self._discovery_running: - return - try: - self._discovery_running = True - self.logger.debug("DLNA discovery started...") - allow_network_scan = self.config.get_value(CONF_NETWORK_SCAN) - discovered_devices: set[str] = set() - - async def on_response(discovery_info: CaseInsensitiveDict) -> None: - """Process discovered device from ssdp search.""" - ssdp_st: str = discovery_info.get("st", discovery_info.get("nt")) - if not ssdp_st: - return - - if "MediaRenderer" not in ssdp_st: - # we're only interested in MediaRenderer devices - return - - ssdp_usn: str = discovery_info["usn"] - ssdp_udn: str | None = discovery_info.get("_udn") - if not ssdp_udn and ssdp_usn.startswith("uuid:"): - ssdp_udn = ssdp_usn.split("::")[0] - - if ssdp_udn in discovered_devices: - # already processed this device - return - if "rincon" in ssdp_udn.lower(): - # ignore Sonos devices - return - - discovered_devices.add(ssdp_udn) - - await self._device_discovered(ssdp_udn, discovery_info["location"]) - - # we iterate between using a regular and multicast search (if enabled) - if allow_network_scan and use_multicast: - await async_search(on_response, target=(str(IPv4Address("255.255.255.255")), 1900)) - else: - await async_search(on_response) - - finally: - self._discovery_running = False - - def reschedule() -> None: - self.mass.create_task(self.discover_players(use_multicast=not use_multicast)) - - # reschedule self once finished - self.mass.loop.call_later(300, reschedule) - - async def _device_disconnect(self, dlna_player: DLNAPlayer) -> None: - """ - Destroy connections to the device now that it's not available. - - Also call when removing this entity from MA to clean up connections. - """ - async with dlna_player.lock: - if not dlna_player.device: - self.logger.debug("Disconnecting from device that's not connected") - return - - self.logger.debug("Disconnecting from %s", dlna_player.device.name) - - dlna_player.device.on_event = None - old_device = dlna_player.device - dlna_player.device = None - await old_device.async_unsubscribe_services() - - async def _device_discovered(self, udn: str, description_url: str) -> None: - """Handle discovered DLNA player.""" - async with self.lock: - if dlna_player := self.dlnaplayers.get(udn): - # existing player - if dlna_player.description_url == description_url and dlna_player.player.available: - # nothing to do, device is already connected - return - # update description url to newly discovered one - dlna_player.description_url = description_url - else: - # new player detected, setup our DLNAPlayer wrapper - conf_key = f"{CONF_PLAYERS}/{udn}/enabled" - enabled = self.mass.config.get(conf_key, True) - # ignore disabled players - if not enabled: - self.logger.debug("Ignoring disabled player: %s", udn) - return - - dlna_player = DLNAPlayer( - udn=udn, - player=Player( - player_id=udn, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=udn, - available=False, - powered=False, - # device info will be discovered later after connect - device_info=DeviceInfo( - model="unknown", - address=description_url, - manufacturer="unknown", - ), - needs_poll=True, - poll_interval=30, - ), - description_url=description_url, - ) - self.dlnaplayers[udn] = dlna_player - - await self._device_connect(dlna_player) - - self._set_player_features(dlna_player) - dlna_player.update_attributes() - await self.mass.players.register_or_update(dlna_player.player) - - async def _device_connect(self, dlna_player: DLNAPlayer) -> None: - """Connect DLNA/DMR Device.""" - self.logger.debug("Connecting to device at %s", dlna_player.description_url) - - async with dlna_player.lock: - if dlna_player.device: - self.logger.debug("Trying to connect when device already connected") - return - - # Connect to the base UPNP device - upnp_device = await self.upnp_factory.async_create_device(dlna_player.description_url) - - # Create profile wrapper - dlna_player.device = DmrDevice(upnp_device, self.notify_server.event_handler) - - # Subscribe to event notifications - try: - dlna_player.device.on_event = self._handle_event - await dlna_player.device.async_subscribe_services(auto_resubscribe=True) - except UpnpResponseError as err: - # Device rejected subscription request. This is OK, variables - # will be polled instead. - self.logger.debug("Device rejected subscription: %r", err) - except UpnpError as err: - # Don't leave the device half-constructed - dlna_player.device.on_event = None - dlna_player.device = None - self.logger.debug("Error while subscribing during device connect: %r", err) - raise - else: - # connect was successful, update device info - dlna_player.player.device_info = DeviceInfo( - model=dlna_player.device.model_name, - address=dlna_player.device.device.presentation_url - or dlna_player.description_url, - manufacturer=dlna_player.device.manufacturer, - ) - - def _handle_event( - self, - service: UpnpService, - state_variables: Sequence[UpnpStateVariable], - ) -> None: - """Handle state variable(s) changed event from DLNA device.""" - udn = service.device.udn - dlna_player = self.dlnaplayers[udn] - - if not state_variables: - # Indicates a failure to resubscribe, check if device is still available - dlna_player.force_poll = True - return - - if service.service_id == "urn:upnp-org:serviceId:AVTransport": - for state_variable in state_variables: - # Force a state refresh when player begins or pauses playback - # to update the position info. - if state_variable.name == "TransportState" and state_variable.value in ( - TransportState.PLAYING, - TransportState.PAUSED_PLAYBACK, - ): - dlna_player.force_poll = True - self.mass.create_task(self.poll_player(dlna_player.udn)) - self.logger.debug( - "Received new state from event for Player %s: %s", - dlna_player.player.display_name, - state_variable.value, - ) - - dlna_player.last_seen = time.time() - self.mass.create_task(self._update_player(dlna_player)) - - async def _update_player(self, dlna_player: DLNAPlayer) -> None: - """Update DLNA Player.""" - prev_url = dlna_player.player.current_item_id - prev_state = dlna_player.player.state - dlna_player.update_attributes() - current_url = dlna_player.player.current_item_id - current_state = dlna_player.player.state - - if (prev_url != current_url) or (prev_state != current_state): - # fetch track details on state or url change - dlna_player.force_poll = True - - # let the MA player manager work out if something actually updated - self.mass.players.update(dlna_player.udn) - - def _set_player_features(self, dlna_player: DLNAPlayer) -> None: - """Set Player Features based on config values and capabilities.""" - supported_features: set[PlayerFeature] = set() - if not self.mass.config.get_raw_player_config_value( - dlna_player.udn, - CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED.key, - CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED.default_value, - ): - supported_features.add(PlayerFeature.ENQUEUE) - - if dlna_player.device.has_volume_level: - supported_features.add(PlayerFeature.VOLUME_SET) - if dlna_player.device.has_volume_mute: - supported_features.add(PlayerFeature.VOLUME_MUTE) - if dlna_player.device.has_pause: - supported_features.add(PlayerFeature.PAUSE) - dlna_player.player.supported_features = tuple(supported_features) diff --git a/music_assistant/server/providers/dlna/helpers.py b/music_assistant/server/providers/dlna/helpers.py deleted file mode 100644 index f59f98de..00000000 --- a/music_assistant/server/providers/dlna/helpers.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Various helpers and utils for the DLNA Player Provider.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from aiohttp.web import Request, Response -from async_upnp_client.const import HttpRequest -from async_upnp_client.event_handler import UpnpEventHandler, UpnpNotifyServer - -if TYPE_CHECKING: - from async_upnp_client.client import UpnpRequester - - from music_assistant.server import MusicAssistant - - -class DLNANotifyServer(UpnpNotifyServer): - """Notify server for async_upnp_client which uses the MA webserver.""" - - def __init__( - self, - requester: UpnpRequester, - mass: MusicAssistant, - ) -> None: - """Initialize.""" - self.mass = mass - self.event_handler = UpnpEventHandler(self, requester) - self.mass.streams.register_dynamic_route("/notify", self._handle_request, method="NOTIFY") - - async def _handle_request(self, request: Request) -> Response: - """Handle incoming requests.""" - if request.method != "NOTIFY": - return Response(status=405) - - # transform aiohttp request to async_upnp_client request - http_request = HttpRequest( - method=request.method, - url=request.url, - headers=request.headers, - body=await request.text(), - ) - - status = await self.event_handler.handle_notify(http_request) - - return Response(status=status) - - @property - def callback_url(self) -> str: - """Return callback URL on which we are callable.""" - return f"{self.mass.streams.base_url}/notify" diff --git a/music_assistant/server/providers/dlna/icon.svg b/music_assistant/server/providers/dlna/icon.svg deleted file mode 100644 index 10e19efa..00000000 --- a/music_assistant/server/providers/dlna/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/music_assistant/server/providers/dlna/manifest.json b/music_assistant/server/providers/dlna/manifest.json deleted file mode 100644 index 9c7e4817..00000000 --- a/music_assistant/server/providers/dlna/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "player", - "domain": "dlna", - "name": "UPnP/DLNA Player provider", - "description": "Support for players that are compatible with the UPnP/DLNA (DMR) standard.", - "codeowners": [ - "@music-assistant" - ], - "requirements": [ - "async-upnp-client==0.41.0" - ], - "documentation": "https://music-assistant.io/player-support/dlna/", - "multi_instance": false, - "builtin": false, - "icon": "dlna" -} diff --git a/music_assistant/server/providers/fanarttv/__init__.py b/music_assistant/server/providers/fanarttv/__init__.py deleted file mode 100644 index cdb72c60..00000000 --- a/music_assistant/server/providers/fanarttv/__init__.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Fanart.tv Metadata provider for Music Assistant.""" - -from __future__ import annotations - -from json import JSONDecodeError -from typing import TYPE_CHECKING - -import aiohttp.client_exceptions - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature -from music_assistant.common.models.media_items import ImageType, MediaItemImage, MediaItemMetadata -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.metadata_provider import MetadataProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.media_items import Album, Artist - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -SUPPORTED_FEATURES = ( - ProviderFeature.ARTIST_METADATA, - ProviderFeature.ALBUM_METADATA, -) - -CONF_ENABLE_ARTIST_IMAGES = "enable_artist_images" -CONF_ENABLE_ALBUM_IMAGES = "enable_album_images" -CONF_CLIENT_KEY = "client_key" - -IMG_MAPPING = { - "artistthumb": ImageType.THUMB, - "hdmusiclogo": ImageType.LOGO, - "musicbanner": ImageType.BANNER, - "artistbackground": ImageType.FANART, -} - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return FanartTvMetadataProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_ENABLE_ARTIST_IMAGES, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of artist images.", - default_value=True, - ), - ConfigEntry( - key=CONF_ENABLE_ALBUM_IMAGES, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of album image(s).", - default_value=True, - ), - ConfigEntry( - key=CONF_CLIENT_KEY, - type=ConfigEntryType.SECURE_STRING, - label="VIP Member Personal API Key (optional)", - description="Support this metadata provider by becoming a VIP Member, " - "resulting in higher rate limits and faster response times among other benefits. " - "See https://wiki.fanart.tv/General/personal%20api/ for more information.", - required=False, - ), - ) - - -class FanartTvMetadataProvider(MetadataProvider): - """Fanart.tv Metadata provider.""" - - throttler: Throttler - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.cache = self.mass.cache - if self.config.get_value(CONF_CLIENT_KEY): - # loosen the throttler when a personal client key is used - self.throttler = Throttler(rate_limit=1, period=1) - else: - self.throttler = Throttler(rate_limit=1, period=30) - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: - """Retrieve metadata for artist on fanart.tv.""" - if not artist.mbid: - return None - if not self.config.get_value(CONF_ENABLE_ARTIST_IMAGES): - return None - self.logger.debug("Fetching metadata for Artist %s on Fanart.tv", artist.name) - if data := await self._get_data(f"music/{artist.mbid}"): - metadata = MediaItemMetadata() - metadata.images = [] - for key, img_type in IMG_MAPPING.items(): - items = data.get(key) - if not items: - continue - for item in items: - metadata.images.append( - MediaItemImage( - type=img_type, - path=item["url"], - provider=self.domain, - remotely_accessible=True, - ) - ) - return metadata - return None - - async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: - """Retrieve metadata for album on fanart.tv.""" - if (mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP)) is None: - return None - if not self.config.get_value(CONF_ENABLE_ALBUM_IMAGES): - return None - self.logger.debug("Fetching metadata for Album %s on Fanart.tv", album.name) - if data := await self._get_data(f"music/albums/{mbid}"): - if data and data.get("albums"): - data = data["albums"][mbid] - metadata = MediaItemMetadata() - metadata.images = [] - for key, img_type in IMG_MAPPING.items(): - items = data.get(key) - if not items: - continue - for item in items: - metadata.images.append( - MediaItemImage( - type=img_type, - path=item["url"], - provider=self.domain, - remotely_accessible=True, - ) - ) - return metadata - return None - - @use_cache(86400 * 30) - async def _get_data(self, endpoint, **kwargs) -> dict | None: - """Get data from api.""" - url = f"http://webservice.fanart.tv/v3/{endpoint}" - headers = { - "api-key": app_var(4), - } - if client_key := self.config.get_value(CONF_CLIENT_KEY): - headers["client_key"] = client_key - async with ( - self.throttler, - self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, - ): - try: - result = await response.json() - except ( - aiohttp.client_exceptions.ContentTypeError, - JSONDecodeError, - ): - self.logger.error("Failed to retrieve %s", endpoint) - text_result = await response.text() - self.logger.debug(text_result) - return None - except ( - aiohttp.client_exceptions.ClientConnectorError, - aiohttp.client_exceptions.ServerDisconnectedError, - ): - self.logger.warning("Failed to retrieve %s", endpoint) - return None - if "error" in result and "limit" in result["error"]: - self.logger.warning(result["error"]) - return None - return result diff --git a/music_assistant/server/providers/fanarttv/manifest.json b/music_assistant/server/providers/fanarttv/manifest.json deleted file mode 100644 index a39b2593..00000000 --- a/music_assistant/server/providers/fanarttv/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "metadata", - "domain": "fanarttv", - "name": "fanart.tv", - "description": "fanart.tv is a community database of artwork for movies, tv series and music.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "", - "multi_instance": false, - "builtin": true, - "icon": "folder-information" -} diff --git a/music_assistant/server/providers/filesystem_local/__init__.py b/music_assistant/server/providers/filesystem_local/__init__.py deleted file mode 100644 index 12c4d28d..00000000 --- a/music_assistant/server/providers/filesystem_local/__init__.py +++ /dev/null @@ -1,1134 +0,0 @@ -"""Filesystem musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import contextlib -import logging -import os -import os.path -from typing import TYPE_CHECKING, cast - -import aiofiles -import shortuuid -import xmltodict -from aiofiles.os import wrap - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - MediaNotFoundError, - MusicAssistantError, - SetupFailedError, -) -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - BrowseFolder, - ContentType, - ImageType, - ItemMapping, - MediaItemImage, - MediaItemType, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, - UniqueList, - is_track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import ( - CONF_PATH, - DB_TABLE_ALBUM_ARTISTS, - DB_TABLE_ALBUM_TRACKS, - DB_TABLE_ALBUMS, - DB_TABLE_ARTISTS, - DB_TABLE_PROVIDER_MAPPINGS, - DB_TABLE_TRACK_ARTISTS, - VARIOUS_ARTISTS_MBID, - VARIOUS_ARTISTS_NAME, -) -from music_assistant.server.helpers.compare import compare_strings, create_safe_string -from music_assistant.server.helpers.playlists import parse_m3u, parse_pls -from music_assistant.server.helpers.tags import AudioTags, parse_tags, split_items -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.music_provider import MusicProvider - -from .helpers import ( - FileSystemItem, - get_absolute_path, - get_album_dir, - get_artist_dir, - get_relative_path, - sorted_scandir, -) - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -CONF_MISSING_ALBUM_ARTIST_ACTION = "missing_album_artist_action" - -CONF_ENTRY_MISSING_ALBUM_ARTIST = ConfigEntry( - key=CONF_MISSING_ALBUM_ARTIST_ACTION, - type=ConfigEntryType.STRING, - label="Action when a track is missing the Albumartist ID3 tag", - default_value="various_artists", - help_link="https://music-assistant.io/music-providers/filesystem/#tagging-files", - required=False, - options=( - ConfigValueOption("Use Track artist(s)", "track_artist"), - ConfigValueOption("Use Various Artists", "various_artists"), - ConfigValueOption("Use Folder name (if possible)", "folder_name"), - ), -) - -TRACK_EXTENSIONS = ( - "mp3", - "m4a", - "m4b", - "mp4", - "flac", - "wav", - "ogg", - "aiff", - "wma", - "dsf", - "opus", -) -PLAYLIST_EXTENSIONS = ("m3u", "pls", "m3u8") -SUPPORTED_EXTENSIONS = TRACK_EXTENSIONS + PLAYLIST_EXTENSIONS -IMAGE_EXTENSIONS = ("jpg", "jpeg", "JPG", "JPEG", "png", "PNG", "gif", "GIF") -SEEKABLE_FILES = (ContentType.MP3, ContentType.WAV, ContentType.FLAC) - - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, -) - -listdir = wrap(os.listdir) -isdir = wrap(os.path.isdir) -isfile = wrap(os.path.isfile) -exists = wrap(os.path.exists) -makedirs = wrap(os.makedirs) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - conf_path = config.get_value(CONF_PATH) - if not await isdir(conf_path): - msg = f"Music Directory {conf_path} does not exist" - raise SetupFailedError(msg) - prov = LocalFileSystemProvider(mass, manifest, config) - prov.base_path = str(config.get_value(CONF_PATH)) - await prov.check_write_access() - return prov - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry(key="path", type=ConfigEntryType.STRING, label="Path", default_value="/media"), - CONF_ENTRY_MISSING_ALBUM_ARTIST, - ) - - -class LocalFileSystemProvider(MusicProvider): - """ - Implementation of a musicprovider for (local) files. - - Reads ID3 tags from file and falls back to parsing filename. - Optionally reads metadata from nfo files and images in folder structure /. - Supports m3u files for playlists. - """ - - base_path: str - write_access: bool = False - scan_limiter = asyncio.Semaphore(25) - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - if self.write_access: - return ( - *SUPPORTED_FEATURES, - ProviderFeature.PLAYLIST_CREATE, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) - return SUPPORTED_FEATURES - - @property - def is_streaming_provider(self) -> bool: - """Return True if the provider is a streaming provider.""" - return False - - async def search( - self, - search_query: str, - media_types: list[MediaType] | None, - limit: int = 5, - ) -> SearchResults: - """Perform search on this file based musicprovider.""" - result = SearchResults() - # searching the filesystem is slow and unreliable, - # so instead we just query the db... - if media_types is None or MediaType.TRACK in media_types: - result.tracks = await self.mass.music.tracks._get_library_items_by_query( - search=search_query, provider=self.instance_id, limit=limit - ) - - if media_types is None or MediaType.ALBUM in media_types: - result.albums = await self.mass.music.albums._get_library_items_by_query( - search=search_query, - provider=self.instance_id, - limit=limit, - ) - - if media_types is None or MediaType.ARTIST in media_types: - result.artists = await self.mass.music.artists._get_library_items_by_query( - search=search_query, - provider=self.instance_id, - limit=limit, - ) - if media_types is None or MediaType.PLAYLIST in media_types: - result.playlists = await self.mass.music.playlists._get_library_items_by_query( - search=search_query, - provider=self.instance_id, - limit=limit, - ) - return result - - async def browse(self, path: str) -> list[MediaItemType | ItemMapping]: - """Browse this provider's items. - - :param path: The path to browse, (e.g. provid://artists). - """ - items: list[MediaItemType | ItemMapping] = [] - item_path = path.split("://", 1)[1] - if not item_path: - item_path = "" - async for item in self.listdir(item_path, recursive=False, sort=True): - if not item.is_dir and ("." not in item.filename or not item.ext): - # skip system files and files without extension - continue - - if item.is_dir: - items.append( - BrowseFolder( - item_id=item.path, - provider=self.instance_id, - path=f"{self.instance_id}://{item.path}", - name=item.filename, - ) - ) - elif item.ext in TRACK_EXTENSIONS: - items.append( - ItemMapping( - media_type=MediaType.TRACK, - item_id=item.path, - provider=self.instance_id, - name=item.filename, - ) - ) - elif item.ext in PLAYLIST_EXTENSIONS: - items.append( - ItemMapping( - media_type=MediaType.PLAYLIST, - item_id=item.path, - provider=self.instance_id, - name=item.filename, - ) - ) - return items - - async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: - """Run library sync for this provider.""" - assert self.mass.music.database - file_checksums: dict[str, str] = {} - query = ( - f"SELECT provider_item_id, details FROM {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE provider_instance = '{self.instance_id}' " - "AND media_type in ('track', 'playlist')" - ) - for db_row in await self.mass.music.database.get_rows_from_query(query, limit=0): - file_checksums[db_row["provider_item_id"]] = str(db_row["details"]) - # find all music files in the music directory and all subfolders - # we work bottom up, as-in we derive all info from the tracks - cur_filenames = set() - prev_filenames = set(file_checksums.keys()) - async with TaskManager(self.mass, 25) as tm: - async for item in self.listdir("", recursive=True, sort=False): - if "." not in item.filename or not item.ext: - # skip system files and files without extension - continue - - if item.ext not in SUPPORTED_EXTENSIONS: - # unsupported file extension - continue - - cur_filenames.add(item.path) - - # continue if the item did not change (checksum still the same) - prev_checksum = file_checksums.get(item.path) - if item.checksum == prev_checksum: - continue - - await tm.create_task_with_limit(self._process_item(item, prev_checksum)) - - # work out deletions - deleted_files = prev_filenames - cur_filenames - await self._process_deletions(deleted_files) - - # process orphaned albums and artists - await self._process_orphaned_albums_and_artists() - - async def _process_item(self, item: FileSystemItem, prev_checksum: str | None) -> None: - """Process a single item.""" - try: - self.logger.debug("Processing: %s", item.path) - if item.ext in TRACK_EXTENSIONS: - # add/update track to db - # note that filesystem items are always overwriting existing info - # when they are detected as changed - track = await self._parse_track(item) - await self.mass.music.tracks.add_item_to_library( - track, overwrite_existing=prev_checksum is not None - ) - elif item.ext in PLAYLIST_EXTENSIONS: - playlist = await self.get_playlist(item.path) - # add/update] playlist to db - playlist.cache_checksum = item.checksum - # playlist is always favorite - playlist.favorite = True - await self.mass.music.playlists.add_item_to_library( - playlist, - overwrite_existing=prev_checksum is not None, - ) - except Exception as err: - # we don't want the whole sync to crash on one file so we catch all exceptions here - self.logger.error( - "Error processing %s - %s", - item.path, - str(err), - exc_info=err if self.logger.isEnabledFor(logging.DEBUG) else None, - ) - - async def _process_orphaned_albums_and_artists(self) -> None: - """Process deletion of orphaned albums and artists.""" - assert self.mass.music.database - # Remove albums without any tracks - query = ( - f"SELECT item_id FROM {DB_TABLE_ALBUMS} " - f"WHERE item_id not in ( SELECT album_id from {DB_TABLE_ALBUM_TRACKS}) " - f"AND item_id in ( SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE provider_instance = '{self.instance_id}' and media_type = 'album' )" - ) - for db_row in await self.mass.music.database.get_rows_from_query( - query, - limit=100000, - ): - await self.mass.music.albums.remove_item_from_library(db_row["item_id"]) - - # Remove artists without any tracks or albums - query = ( - f"SELECT item_id FROM {DB_TABLE_ARTISTS} " - f"WHERE item_id not in " - f"( select artist_id from {DB_TABLE_TRACK_ARTISTS} " - f"UNION SELECT artist_id from {DB_TABLE_ALBUM_ARTISTS} ) " - f"AND item_id in ( SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} " - f"WHERE provider_instance = '{self.instance_id}' and media_type = 'artist' )" - ) - for db_row in await self.mass.music.database.get_rows_from_query( - query, - limit=100000, - ): - await self.mass.music.artists.remove_item_from_library(db_row["item_id"]) - - async def _process_deletions(self, deleted_files: set[str]) -> None: - """Process all deletions.""" - # process deleted tracks/playlists - album_ids = set() - artist_ids = set() - for file_path in deleted_files: - _, ext = file_path.rsplit(".", 1) - if ext not in SUPPORTED_EXTENSIONS: - # unsupported file extension - continue - - if ext in PLAYLIST_EXTENSIONS: - controller = self.mass.music.get_controller(MediaType.PLAYLIST) - else: - controller = self.mass.music.get_controller(MediaType.TRACK) - - if library_item := await controller.get_library_item_by_prov_id( - file_path, self.instance_id - ): - if is_track(library_item): - if library_item.album: - album_ids.add(library_item.album.item_id) - # need to fetch the library album to resolve the itemmapping - db_album = await self.mass.music.albums.get_library_item( - library_item.album.item_id - ) - for artist in db_album.artists: - artist_ids.add(artist.item_id) - for artist in library_item.artists: - artist_ids.add(artist.item_id) - await controller.remove_item_from_library(library_item.item_id) - # check if any albums need to be cleaned up - for album_id in album_ids: - if not await self.mass.music.albums.tracks(album_id, "library"): - await self.mass.music.albums.remove_item_from_library(album_id) - # check if any artists need to be cleaned up - for artist_id in artist_ids: - artist_albums = await self.mass.music.artists.albums(artist_id, "library") - artist_tracks = await self.mass.music.artists.tracks(artist_id, "library") - if not (artist_albums or artist_tracks): - await self.mass.music.artists.remove_item_from_library(artist_id) - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - db_artist = await self.mass.music.artists.get_library_item_by_prov_id( - prov_artist_id, self.instance_id - ) - if not db_artist: - # this should not be possible, but just in case - msg = f"Artist not found: {prov_artist_id}" - raise MediaNotFoundError(msg) - # prov_artist_id is either an actual (relative) path or a name (as fallback) - safe_artist_name = create_safe_string(prov_artist_id, lowercase=False, replace_space=False) - if await self.exists(prov_artist_id): - artist_path = prov_artist_id - elif await self.exists(safe_artist_name): - artist_path = safe_artist_name - else: - for prov_mapping in db_artist.provider_mappings: - if prov_mapping.provider_instance != self.instance_id: - continue - if prov_mapping.url: - artist_path = prov_mapping.url - break - else: - # this is an artist without an actual path on disk - # return the info we already have in the db - return db_artist - return await self._parse_artist( - db_artist.name, - sort_name=db_artist.sort_name, - mbid=db_artist.mbid, - artist_path=artist_path, - ) - - async def get_album(self, prov_album_id: str) -> Album: - """Get full album details by id.""" - for track in await self.get_album_tracks(prov_album_id): - for prov_mapping in track.provider_mappings: - if prov_mapping.provider_instance == self.instance_id: - file_item = await self.resolve(prov_mapping.item_id) - full_track = await self._parse_track(file_item) - assert isinstance(full_track.album, Album) - return full_track.album - msg = f"Album not found: {prov_album_id}" - raise MediaNotFoundError(msg) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - # ruff: noqa: PLR0915, PLR0912 - if not await self.exists(prov_track_id): - msg = f"Track path does not exist: {prov_track_id}" - raise MediaNotFoundError(msg) - - file_item = await self.resolve(prov_track_id) - return await self._parse_track(file_item, full_album_metadata=True) - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id.""" - if not await self.exists(prov_playlist_id): - msg = f"Playlist path does not exist: {prov_playlist_id}" - raise MediaNotFoundError(msg) - - file_item = await self.resolve(prov_playlist_id) - playlist = Playlist( - item_id=file_item.path, - provider=self.instance_id, - name=file_item.name, - provider_mappings={ - ProviderMapping( - item_id=file_item.path, - provider_domain=self.domain, - provider_instance=self.instance_id, - details=file_item.checksum, - ) - }, - ) - playlist.is_editable = ProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features - # only playlists in the root are editable - all other are read only - if "/" in prov_playlist_id or "\\" in prov_playlist_id: - playlist.is_editable = False - # we do not (yet) have support to edit/create pls playlists, only m3u files can be edited - if file_item.ext == "pls": - playlist.is_editable = False - playlist.owner = self.name - checksum = str(file_item.checksum) - playlist.cache_checksum = checksum - return playlist - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get album tracks for given album id.""" - # filesystem items are always stored in db so we can query the database - db_album = await self.mass.music.albums.get_library_item_by_prov_id( - prov_album_id, self.instance_id - ) - if db_album is None: - msg = f"Album not found: {prov_album_id}" - raise MediaNotFoundError(msg) - album_tracks = await self.mass.music.albums.get_library_album_tracks(db_album.item_id) - return [ - track - for track in album_tracks - if any(x.provider_instance == self.instance_id for x in track.provider_mappings) - ] - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # paging not (yet) supported - return result - if not await self.exists(prov_playlist_id): - msg = f"Playlist path does not exist: {prov_playlist_id}" - raise MediaNotFoundError(msg) - - _, ext = prov_playlist_id.rsplit(".", 1) - try: - # get playlist file contents - playlist_filename = self.get_absolute_path(prov_playlist_id) - async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: - playlist_data = await _file.read() - if ext in ("m3u", "m3u8"): - playlist_lines = parse_m3u(playlist_data) - else: - playlist_lines = parse_pls(playlist_data) - - for idx, playlist_line in enumerate(playlist_lines, 1): - if track := await self._parse_playlist_line( - playlist_line.path, os.path.dirname(prov_playlist_id) - ): - track.position = idx - result.append(track) - - except Exception as err: - self.logger.warning( - "Error while parsing playlist %s: %s", - prov_playlist_id, - str(err), - exc_info=err if self.logger.isEnabledFor(10) else None, - ) - return result - - async def _parse_playlist_line(self, line: str, playlist_path: str) -> Track | None: - """Try to parse a track from a playlist line.""" - try: - # if a relative path was given in an upper level from the playlist, - # try to resolve it - for parentpart in ("../", "..\\"): - while line.startswith(parentpart): - if len(playlist_path) < 3: - break # guard - playlist_path = parentpart[:-3] - line = line[3:] - - # try to resolve the filename - for filename in (line, os.path.join(playlist_path, line)): - with contextlib.suppress(FileNotFoundError): - item = await self.resolve(filename) - return await self._parse_track(item) - - except MusicAssistantError as err: - self.logger.warning("Could not parse uri/file %s to track: %s", line, str(err)) - - return None - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - if not await self.exists(prov_playlist_id): - msg = f"Playlist path does not exist: {prov_playlist_id}" - raise MediaNotFoundError(msg) - playlist_filename = self.get_absolute_path(prov_playlist_id) - async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: - playlist_data = await _file.read() - for file_path in prov_track_ids: - track = await self.get_track(file_path) - playlist_data += f"\n#EXTINF:{track.duration or 0},{track.name}\n{file_path}\n" - - # write playlist file (always in utf-8) - async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: - await _file.write(playlist_data) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - if not await self.exists(prov_playlist_id): - msg = f"Playlist path does not exist: {prov_playlist_id}" - raise MediaNotFoundError(msg) - _, ext = prov_playlist_id.rsplit(".", 1) - # get playlist file contents - playlist_filename = self.get_absolute_path(prov_playlist_id) - async with aiofiles.open(playlist_filename, encoding="utf-8") as _file: - playlist_data = await _file.read() - # get current contents first - if ext in ("m3u", "m3u8"): - playlist_items = parse_m3u(playlist_data) - else: - playlist_items = parse_pls(playlist_data) - # remove items by index - for i in sorted(positions_to_remove, reverse=True): - # position = index + 1 - del playlist_items[i - 1] - # build new playlist data - new_playlist_data = "#EXTM3U\n" - for item in playlist_items: - new_playlist_data += f"\n#EXTINF:{item.length or 0},{item.title}\n{item.path}\n" - async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: - await _file.write(playlist_data) - - async def create_playlist(self, name: str) -> Playlist: - """Create a new playlist on provider with given name.""" - # creating a new playlist on the filesystem is as easy - # as creating a new (empty) file with the m3u extension... - # filename = await self.resolve(f"{name}.m3u") - filename = f"{name}.m3u" - playlist_filename = self.get_absolute_path(filename) - async with aiofiles.open(playlist_filename, "w", encoding="utf-8") as _file: - await _file.write("#EXTM3U\n") - return await self.get_playlist(filename) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - library_item = await self.mass.music.tracks.get_library_item_by_prov_id( - item_id, self.instance_id - ) - if library_item is None: - # this could be a file that has just been added, try parsing it - file_item = await self.resolve(item_id) - if not (library_item := await self._parse_track(file_item)): - msg = f"Item not found: {item_id}" - raise MediaNotFoundError(msg) - - prov_mapping = next(x for x in library_item.provider_mappings if x.item_id == item_id) - file_item = await self.resolve(item_id) - - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=prov_mapping.audio_format, - media_type=MediaType.TRACK, - stream_type=StreamType.LOCAL_FILE, - duration=library_item.duration, - size=file_item.file_size, - data=file_item, - path=file_item.absolute_path, - can_seek=True, - ) - - async def resolve_image(self, path: str) -> str | bytes: - """ - Resolve an image from an image path. - - This either returns (a generator to get) raw bytes of the image or - a string with an http(s) URL or local path that is accessible from the server. - """ - file_item = await self.resolve(path) - return file_item.absolute_path - - async def _parse_track( - self, file_item: FileSystemItem, full_album_metadata: bool = False - ) -> Track: - """Get full track details by id.""" - # ruff: noqa: PLR0915, PLR0912 - - # parse tags - tags = await parse_tags(file_item.absolute_path, file_item.file_size) - name, version = parse_title_and_version(tags.title, tags.version) - track = Track( - item_id=file_item.path, - provider=self.instance_id, - name=name, - sort_name=tags.title_sort, - version=version, - provider_mappings={ - ProviderMapping( - item_id=file_item.path, - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(tags.format), - sample_rate=tags.sample_rate, - bit_depth=tags.bits_per_sample, - channels=tags.channels, - bit_rate=tags.bit_rate, - ), - details=file_item.checksum, - ) - }, - disc_number=tags.disc or 0, - track_number=tags.track or 0, - ) - - if isrc_tags := tags.isrc: - for isrsc in isrc_tags: - track.external_ids.add((ExternalID.ISRC, isrsc)) - - if acoustid := tags.get("acoustidid"): - track.external_ids.add((ExternalID.ACOUSTID, acoustid)) - - # album - album = track.album = ( - await self._parse_album(track_path=file_item.path, track_tags=tags) - if tags.album - else None - ) - - # track artist(s) - for index, track_artist_str in enumerate(tags.artists): - # prefer album artist if match - if album and ( - album_artist_match := next( - (x for x in album.artists if x.name == track_artist_str), None - ) - ): - track.artists.append(album_artist_match) - continue - artist = await self._parse_artist( - track_artist_str, - sort_name=( - tags.artist_sort_names[index] if index < len(tags.artist_sort_names) else None - ), - mbid=( - tags.musicbrainz_artistids[index] - if index < len(tags.musicbrainz_artistids) - else None - ), - ) - track.artists.append(artist) - - # handle embedded cover image - if tags.has_cover_image: - # we do not actually embed the image in the metadata because that would consume too - # much space and bandwidth. Instead we set the filename as value so the image can - # be retrieved later in realtime. - track.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=file_item.path, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - ) - - # copy (embedded) album image from track (if the album itself doesn't have an image) - if album and not album.image and track.image: - album.metadata.images = UniqueList([track.image]) - - # parse other info - track.duration = int(tags.duration or 0) - track.metadata.genres = set(tags.genres) - if tags.disc: - track.disc_number = tags.disc - if tags.track: - track.track_number = tags.track - track.metadata.copyright = tags.get("copyright") - track.metadata.lyrics = tags.lyrics - explicit_tag = tags.get("itunesadvisory") - if explicit_tag is not None: - track.metadata.explicit = explicit_tag == "1" - if tags.musicbrainz_recordingid: - track.mbid = tags.musicbrainz_recordingid - track.metadata.chapters = UniqueList(tags.chapters) - # handle (optional) loudness measurement tag(s) - if tags.track_loudness is not None: - await self.mass.music.set_loudness( - track.item_id, self.instance_id, tags.track_loudness, tags.track_album_loudness - ) - return track - - async def _parse_artist( - self, - name: str, - album_dir: str | None = None, - sort_name: str | None = None, - mbid: str | None = None, - artist_path: str | None = None, - ) -> Artist: - """Parse full (album) Artist.""" - if not artist_path: - # we need to hunt for the artist (metadata) path on disk - # this can either be relative to the album path or at root level - # check if we have an artist folder for this artist at root level - safe_artist_name = create_safe_string(name, lowercase=False, replace_space=False) - if await self.exists(name): - artist_path = name - elif await self.exists(safe_artist_name): - artist_path = safe_artist_name - elif album_dir and (foldermatch := get_artist_dir(name, album_dir=album_dir)): - # try to find (album)artist folder based on album path - artist_path = foldermatch - else: - # check if we have an existing item to retrieve the artist path - async for item in self.mass.music.artists.iter_library_items(search=name): - if not compare_strings(name, item.name): - continue - for prov_mapping in item.provider_mappings: - if prov_mapping.provider_instance != self.instance_id: - continue - if prov_mapping.url: - artist_path = prov_mapping.url - break - if artist_path: - break - - # prefer (short lived) cache for a bit more speed - cache_base_key = f"{self.instance_id}.artist" - if artist_path and (cache := await self.cache.get(artist_path, base_key=cache_base_key)): - return cast(Artist, cache) - - prov_artist_id = artist_path or name - artist = Artist( - item_id=prov_artist_id, - provider=self.instance_id, - name=name, - sort_name=sort_name, - provider_mappings={ - ProviderMapping( - item_id=prov_artist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=artist_path, - ) - }, - ) - if mbid: - artist.mbid = mbid - if not artist_path: - return artist - - # grab additional metadata within the Artist's folder - nfo_file = os.path.join(artist_path, "artist.nfo") - if await self.exists(nfo_file): - # found NFO file with metadata - # https://kodi.wiki/view/NFO_files/Artists - nfo_file = self.get_absolute_path(nfo_file) - async with aiofiles.open(nfo_file) as _file: - data = await _file.read() - info = await asyncio.to_thread(xmltodict.parse, data) - info = info["artist"] - artist.name = info.get("title", info.get("name", name)) - if sort_name := info.get("sortname"): - artist.sort_name = sort_name - if mbid := info.get("musicbrainzartistid"): - artist.mbid = mbid - if description := info.get("biography"): - artist.metadata.description = description - if genre := info.get("genre"): - artist.metadata.genres = set(split_items(genre)) - # find local images - if images := await self._get_local_images(artist_path): - artist.metadata.images = UniqueList(images) - - await self.cache.set(artist_path, artist, base_key=cache_base_key, expiration=120) - - return artist - - async def _parse_album(self, track_path: str, track_tags: AudioTags) -> Album: - """Parse Album metadata from Track tags.""" - assert track_tags.album - # work out if we have an album and/or disc folder - # track_dir is the folder level where the tracks are located - # this may be a separate disc folder (Disc 1, Disc 2 etc) underneath the album folder - # or this is an album folder with the disc attached - track_dir = os.path.dirname(track_path) - album_dir = get_album_dir(track_dir, track_tags.album) - - cache_base_key = f"{self.instance_id}.album" - if album_dir and (cache := await self.cache.get(album_dir, base_key=cache_base_key)): - return cast(Album, cache) - - # album artist(s) - album_artists: UniqueList[Artist | ItemMapping] = UniqueList() - if track_tags.album_artists: - for index, album_artist_str in enumerate(track_tags.album_artists): - artist = await self._parse_artist( - album_artist_str, - album_dir=album_dir, - sort_name=( - track_tags.album_artist_sort_names[index] - if index < len(track_tags.album_artist_sort_names) - else None - ), - mbid=( - track_tags.musicbrainz_albumartistids[index] - if index < len(track_tags.musicbrainz_albumartistids) - else None - ), - ) - album_artists.append(artist) - else: - # album artist tag is missing, determine fallback - fallback_action = self.config.get_value(CONF_MISSING_ALBUM_ARTIST_ACTION) - if fallback_action == "folder_name" and album_dir: - possible_artist_folder = os.path.dirname(album_dir) - self.logger.warning( - "%s is missing ID3 tag [albumartist], using foldername %s as fallback", - track_path, - possible_artist_folder, - ) - album_artist_str = possible_artist_folder.rsplit(os.sep)[-1] - album_artists = UniqueList( - [await self._parse_artist(name=album_artist_str, album_dir=album_dir)] - ) - # fallback to track artists (if defined by user) - elif fallback_action == "track_artist": - self.logger.warning( - "%s is missing ID3 tag [albumartist], using track artist(s) as fallback", - track_path, - ) - album_artists = UniqueList( - [ - await self._parse_artist(name=track_artist_str, album_dir=album_dir) - for track_artist_str in track_tags.artists - ] - ) - # all other: fallback to various artists - else: - self.logger.warning( - "%s is missing ID3 tag [albumartist], using %s as fallback", - track_path, - VARIOUS_ARTISTS_NAME, - ) - album_artists = UniqueList( - [await self._parse_artist(name=VARIOUS_ARTISTS_NAME, mbid=VARIOUS_ARTISTS_MBID)] - ) - - if album_dir: # noqa: SIM108 - # prefer the path as id - item_id = album_dir - else: - # create fake item_id based on artist + album - item_id = album_artists[0].name + os.sep + track_tags.album - - name, version = parse_title_and_version(track_tags.album) - album = Album( - item_id=item_id, - provider=self.instance_id, - name=name, - version=version, - sort_name=track_tags.album_sort, - artists=album_artists, - provider_mappings={ - ProviderMapping( - item_id=item_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=album_dir, - ) - }, - ) - if track_tags.barcode: - album.external_ids.add((ExternalID.BARCODE, track_tags.barcode)) - - if track_tags.musicbrainz_albumid: - album.mbid = track_tags.musicbrainz_albumid - if track_tags.musicbrainz_releasegroupid: - album.add_external_id(ExternalID.MB_RELEASEGROUP, track_tags.musicbrainz_releasegroupid) - if track_tags.year: - album.year = track_tags.year - album.album_type = track_tags.album_type - - # hunt for additional metadata and images in the folder structure - if not album_dir: - return album - - for folder_path in (track_dir, album_dir): - if not folder_path or not await self.exists(folder_path): - continue - nfo_file = os.path.join(folder_path, "album.nfo") - if await self.exists(nfo_file): - # found NFO file with metadata - # https://kodi.wiki/view/NFO_files/Artists - nfo_file = self.get_absolute_path(nfo_file) - async with aiofiles.open(nfo_file) as _file: - data = await _file.read() - info = await asyncio.to_thread(xmltodict.parse, data) - info = info["album"] - album.name = info.get("title", info.get("name", name)) - if sort_name := info.get("sortname"): - album.sort_name = sort_name - if releasegroup_id := info.get("musicbrainzreleasegroupid"): - album.add_external_id(ExternalID.MB_RELEASEGROUP, releasegroup_id) - if album_id := info.get("musicbrainzalbumid"): - album.add_external_id(ExternalID.MB_ALBUM, album_id) - if mb_artist_id := info.get("musicbrainzalbumartistid"): - if album.artists and not album.artists[0].mbid: - album.artists[0].mbid = mb_artist_id - if description := info.get("review"): - album.metadata.description = description - if year := info.get("year"): - album.year = int(year) - if genre := info.get("genre"): - album.metadata.genres = set(split_items(genre)) - # parse name/version - album.name, album.version = parse_title_and_version(album.name) - # find local images - if images := await self._get_local_images(folder_path): - if album.metadata.images is None: - album.metadata.images = UniqueList(images) - else: - album.metadata.images += images - await self.cache.set(album_dir, album, base_key=cache_base_key, expiration=120) - return album - - async def _get_local_images(self, folder: str) -> UniqueList[MediaItemImage]: - """Return local images found in a given folderpath.""" - images: UniqueList[MediaItemImage] = UniqueList() - async for item in self.listdir(folder): - if "." not in item.path or item.is_dir: - continue - for ext in IMAGE_EXTENSIONS: - if item.ext != ext: - continue - # try match on filename = one of our imagetypes - if item.name in ImageType: - images.append( - MediaItemImage( - type=ImageType(item.name), - path=item.path, - provider=self.instance_id, - remotely_accessible=False, - ) - ) - continue - # try alternative names for thumbs - for filename in ("folder", "cover", "albumart", "artist"): - if item.name.lower().startswith(filename): - images.append( - MediaItemImage( - type=ImageType.THUMB, - path=item.path, - provider=self.instance_id, - remotely_accessible=False, - ) - ) - break - return images - - async def check_write_access(self) -> None: - """Perform check if we have write access.""" - # verify write access to determine we have playlist create/edit support - # overwrite with provider specific implementation if needed - temp_file_name = self.get_absolute_path(f"{shortuuid.random(8)}.txt") - try: - async with aiofiles.open(temp_file_name, "w") as _file: - await _file.write("test") - await asyncio.to_thread(os.remove, temp_file_name) - self.write_access = True - except Exception as err: - self.logger.debug("Write access disabled: %s", str(err)) - - async def listdir( - self, path: str, recursive: bool = False, sort: bool = False - ) -> AsyncGenerator[FileSystemItem, None]: - """List contents of a given provider directory/path. - - Parameters - ---------- - - path: path of the directory (relative or absolute) to list contents of. - Empty string for provider's root. - - recursive: If True will recursively keep unwrapping subdirectories (scandir equivalent). - - Returns: - ------- - AsyncGenerator yielding FileSystemItem objects. - - """ - abs_path = self.get_absolute_path(path) - for entry in await asyncio.to_thread(sorted_scandir, self.base_path, abs_path, sort): - if recursive and entry.is_dir: - try: - async for subitem in self.listdir(entry.absolute_path, True, sort): - yield subitem - except (OSError, PermissionError) as err: - self.logger.warning("Skip folder %s: %s", entry.path, str(err)) - else: - yield entry - - async def resolve( - self, - file_path: str, - ) -> FileSystemItem: - """Resolve (absolute or relative) path to FileSystemItem.""" - absolute_path = self.get_absolute_path(file_path) - - def _create_item() -> FileSystemItem: - stat = os.stat(absolute_path, follow_symlinks=False) - return FileSystemItem( - filename=os.path.basename(file_path), - path=get_relative_path(self.base_path, file_path), - absolute_path=absolute_path, - is_dir=os.path.isdir(absolute_path), - is_file=os.path.isfile(absolute_path), - checksum=str(int(stat.st_mtime)), - file_size=stat.st_size, - ) - - # run in thread because strictly taken this may be blocking IO - return await asyncio.to_thread(_create_item) - - async def exists(self, file_path: str) -> bool: - """Return bool is this FileSystem musicprovider has given file/dir.""" - if not file_path: - return False # guard - abs_path = self.get_absolute_path(file_path) - return bool(await exists(abs_path)) - - def get_absolute_path(self, file_path: str) -> str: - """Return absolute path for given file path.""" - return get_absolute_path(self.base_path, file_path) diff --git a/music_assistant/server/providers/filesystem_local/helpers.py b/music_assistant/server/providers/filesystem_local/helpers.py deleted file mode 100644 index 237699ab..00000000 --- a/music_assistant/server/providers/filesystem_local/helpers.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Some helpers for Filesystem based Musicproviders.""" - -from __future__ import annotations - -import os -import re -from dataclasses import dataclass - -from music_assistant.server.helpers.compare import compare_strings - -IGNORE_DIRS = ("recycle", "Recently-Snaphot") - - -@dataclass -class FileSystemItem: - """Representation of an item (file or directory) on the filesystem. - - - filename: Name (not path) of the file (or directory). - - path: Relative path to the item on this filesystem provider. - - absolute_path: Absolute path to this item. - - is_file: Boolean if item is file (not directory or symlink). - - is_dir: Boolean if item is directory (not file). - - checksum: Checksum for this path (usually last modified time). - - file_size : File size in number of bytes or None if unknown (or not a file). - """ - - filename: str - path: str - absolute_path: str - is_file: bool - is_dir: bool - checksum: str - file_size: int | None = None - - @property - def ext(self) -> str | None: - """Return file extension.""" - try: - return self.filename.rsplit(".", 1)[1] - except IndexError: - return None - - @property - def name(self) -> str: - """Return file name (without extension).""" - return self.filename.rsplit(".", 1)[0] - - -def get_artist_dir( - artist_name: str, - album_dir: str | None, -) -> str | None: - """Look for (Album)Artist directory in path of a track (or album).""" - if not album_dir: - return None - parentdir = os.path.dirname(album_dir) - # account for disc or album sublevel by ignoring (max) 2 levels if needed - matched_dir: str | None = None - for _ in range(3): - dirname = parentdir.rsplit(os.sep)[-1] - if compare_strings(artist_name, dirname, False): - # literal match - # we keep hunting further down to account for the - # edge case where the album name has the same name as the artist - matched_dir = parentdir - parentdir = os.path.dirname(parentdir) - return matched_dir - - -def get_album_dir(track_dir: str, album_name: str) -> str | None: - """Return album/parent directory of a track.""" - parentdir = track_dir - # account for disc sublevel by ignoring 1 level if needed - for _ in range(2): - dirname = parentdir.rsplit(os.sep)[-1] - if compare_strings(album_name, dirname, False): - # literal match - return parentdir - if compare_strings(album_name, dirname.split(" - ")[-1], False): - # account for ArtistName - AlbumName format in the directory name - return parentdir - if compare_strings(album_name, dirname.split(" - ")[-1].split("(")[0], False): - # account for ArtistName - AlbumName (Version) format in the directory name - return parentdir - if compare_strings(album_name.split("(")[0], dirname, False): - # account for AlbumName (Version) format in the album name - return parentdir - if compare_strings(album_name.split("(")[0], dirname.split(" - ")[-1], False): - # account for ArtistName - AlbumName (Version) format - return parentdir - if len(album_name) > 8 and album_name in dirname: - # dirname contains album name - # (could potentially lead to false positives, hence the length check) - return parentdir - parentdir = os.path.dirname(parentdir) - return None - - -def get_relative_path(base_path: str, path: str) -> str: - """Return the relative path string for a path.""" - if path.startswith(base_path): - path = path.split(base_path)[1] - for sep in ("/", "\\"): - if path.startswith(sep): - path = path[1:] - return path - - -def get_absolute_path(base_path: str, path: str) -> str: - """Return the absolute path string for a path.""" - if path.startswith(base_path): - return path - return os.path.join(base_path, path) - - -def sorted_scandir(base_path: str, sub_path: str, sort: bool = False) -> list[FileSystemItem]: - """ - Implement os.scandir that returns (optionally) sorted entries. - - Not async friendly! - """ - - def nat_key(name: str) -> tuple[int | str, ...]: - """Sort key for natural sorting.""" - return tuple(int(s) if s.isdigit() else s for s in re.split(r"(\d+)", name)) - - def create_item(entry: os.DirEntry) -> FileSystemItem: - """Create FileSystemItem from os.DirEntry.""" - absolute_path = get_absolute_path(base_path, entry.path) - stat = entry.stat(follow_symlinks=False) - return FileSystemItem( - filename=entry.name, - path=get_relative_path(base_path, entry.path), - absolute_path=absolute_path, - is_file=entry.is_file(follow_symlinks=False), - is_dir=entry.is_dir(follow_symlinks=False), - checksum=str(int(stat.st_mtime)), - file_size=stat.st_size, - ) - - items = [ - create_item(x) - for x in os.scandir(sub_path) - # filter out invalid dirs and hidden files - if x.name not in IGNORE_DIRS and not x.name.startswith(".") - ] - if sort: - return sorted( - items, - # sort by (natural) name - key=lambda x: nat_key(x.name), - ) - return items diff --git a/music_assistant/server/providers/filesystem_local/manifest.json b/music_assistant/server/providers/filesystem_local/manifest.json deleted file mode 100644 index 7c3ea523..00000000 --- a/music_assistant/server/providers/filesystem_local/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "music", - "domain": "filesystem_local", - "name": "Filesystem (local disk)", - "description": "Support for music files that are present on a local accessible disk/folder.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/music-providers/filesystem/", - "multi_instance": true, - "icon": "harddisk" -} diff --git a/music_assistant/server/providers/filesystem_smb/__init__.py b/music_assistant/server/providers/filesystem_smb/__init__.py deleted file mode 100644 index 1168f2f1..00000000 --- a/music_assistant/server/providers/filesystem_smb/__init__.py +++ /dev/null @@ -1,228 +0,0 @@ -"""SMB filesystem provider for Music Assistant.""" - -from __future__ import annotations - -import os -import platform -from typing import TYPE_CHECKING - -from music_assistant.common.helpers.util import get_ip_from_host -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import LoginFailed -from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.process import check_output -from music_assistant.server.providers.filesystem_local import ( - CONF_ENTRY_MISSING_ALBUM_ARTIST, - LocalFileSystemProvider, - exists, - makedirs, -) - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -CONF_HOST = "host" -CONF_SHARE = "share" -CONF_SUBFOLDER = "subfolder" -CONF_MOUNT_OPTIONS = "mount_options" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - # check if valid dns name is given for the host - server = str(config.get_value(CONF_HOST)) - if not await get_ip_from_host(server): - msg = f"Unable to resolve {server}, make sure the address is resolveable." - raise LoginFailed(msg) - # check if share is valid - share = str(config.get_value(CONF_SHARE)) - if not share or "/" in share or "\\" in share: - msg = "Invalid share name" - raise LoginFailed(msg) - return SMBFileSystemProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_HOST, - type=ConfigEntryType.STRING, - label="Server", - required=True, - description="The (fqdn) hostname of the SMB/CIFS/DFS server to connect to." - "For example mynas.local.", - ), - ConfigEntry( - key=CONF_SHARE, - type=ConfigEntryType.STRING, - label="Share", - required=True, - description="The name of the share/service you'd like to connect to on " - "the remote host, For example 'media'.", - ), - ConfigEntry( - key=CONF_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - default_value="guest", - description="The username to authenticate to the remote server. " - "For anynymous access you may want to try with the user `guest`.", - ), - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password", - required=False, - default_value=None, - description="The username to authenticate to the remote server. " - "For anynymous access you may want to try with the user `guest`.", - ), - ConfigEntry( - key=CONF_SUBFOLDER, - type=ConfigEntryType.STRING, - label="Subfolder", - required=False, - default_value="", - description="[optional] Use if your music is stored in a sublevel of the share. " - "E.g. 'collections' or 'albums/A-K'.", - ), - ConfigEntry( - key=CONF_MOUNT_OPTIONS, - type=ConfigEntryType.STRING, - label="Mount options", - required=False, - category="advanced", - default_value="noserverino,file_mode=0775,dir_mode=0775,uid=0,gid=0", - description="[optional] Any additional mount options you " - "want to pass to the mount command if needed for your particular setup.", - ), - CONF_ENTRY_MISSING_ALBUM_ARTIST, - ) - - -class SMBFileSystemProvider(LocalFileSystemProvider): - """ - Implementation of an SMB File System Provider. - - Basically this is just a wrapper around the regular local files provider, - except for the fact that it will mount a remote folder to a temporary location. - We went for this OS-depdendent approach because there is no solid async-compatible - smb library for Python (and we tried both pysmb and smbprotocol). - """ - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - # base_path will be the path where we're going to mount the remote share - self.base_path = f"/tmp/{self.instance_id}" # noqa: S108 - if not await exists(self.base_path): - await makedirs(self.base_path) - - try: - # do unmount first to cleanup any unexpected state - await self.unmount(ignore_error=True) - await self.mount() - except Exception as err: - msg = f"Connection failed for the given details: {err}" - raise LoginFailed(msg) from err - - await self.check_write_access() - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - await self.unmount() - - async def mount(self) -> None: - """Mount the SMB location to a temporary folder.""" - server = str(self.config.get_value(CONF_HOST)) - username = str(self.config.get_value(CONF_USERNAME)) - password = self.config.get_value(CONF_PASSWORD) - share = str(self.config.get_value(CONF_SHARE)) - - # handle optional subfolder - subfolder = str(self.config.get_value(CONF_SUBFOLDER)) - if subfolder: - subfolder = subfolder.replace("\\", "/") - if not subfolder.startswith("/"): - subfolder = "/" + subfolder - if subfolder.endswith("/"): - subfolder = subfolder[:-1] - - if platform.system() == "Darwin": - # NOTE: MacOS does not support special characters in the username/password - password_str = f":{password}" if password else "" - mount_cmd = [ - "mount", - "-t", - "smbfs", - f"//{username}{password_str}@{server}/{share}{subfolder}", - self.base_path, - ] - - elif platform.system() == "Linux": - options = ["rw"] - if mount_options := str(self.config.get_value(CONF_MOUNT_OPTIONS)): - options += mount_options.split(",") - options_str = ",".join(options) - - # pass the username+password using (scoped) env variables - # to prevent leaking in the process list and special chars supported - env_vars = { - **os.environ, - "USER": username, - } - if password: - env_vars["PASSWD"] = str(password) - - mount_cmd = [ - "mount", - "-t", - "cifs", - "-o", - options_str, - f"//{server}/{share}{subfolder}", - self.base_path, - ] - else: - msg = f"SMB provider is not supported on {platform.system()}" - raise LoginFailed(msg) - - self.logger.debug("Mounting //%s/%s%s to %s", server, share, subfolder, self.base_path) - self.logger.log( - VERBOSE_LOG_LEVEL, - "Using mount command: %s", - " ".join(mount_cmd), - ) - returncode, output = await check_output(*mount_cmd, env=env_vars) - if returncode != 0: - msg = f"SMB mount failed with error: {output.decode()}" - raise LoginFailed(msg) - - async def unmount(self, ignore_error: bool = False) -> None: - """Unmount the remote share.""" - returncode, output = await check_output("umount", self.base_path) - if returncode != 0 and not ignore_error: - self.logger.warning("SMB unmount failed with error: %s", output.decode()) diff --git a/music_assistant/server/providers/filesystem_smb/manifest.json b/music_assistant/server/providers/filesystem_smb/manifest.json deleted file mode 100644 index 53bc716e..00000000 --- a/music_assistant/server/providers/filesystem_smb/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "music", - "domain": "filesystem_smb", - "name": "Filesystem (remote share)", - "description": "Support for music files that are present on remote SMB/CIFS.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/music-providers/filesystem/", - "multi_instance": true, - "icon": "network" - } diff --git a/music_assistant/server/providers/fully_kiosk/__init__.py b/music_assistant/server/providers/fully_kiosk/__init__.py deleted file mode 100644 index 1977ccd2..00000000 --- a/music_assistant/server/providers/fully_kiosk/__init__.py +++ /dev/null @@ -1,218 +0,0 @@ -"""FullyKiosk Player provider for Music Assistant.""" - -from __future__ import annotations - -import asyncio -import logging -import time -from typing import TYPE_CHECKING - -from fullykiosk import FullyKiosk - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, - CONF_ENTRY_FLOW_MODE_ENFORCED, - ConfigEntry, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import PlayerUnavailableError, SetupFailedError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import ( - CONF_ENFORCE_MP3, - CONF_IP_ADDRESS, - CONF_PASSWORD, - CONF_PORT, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.models.player_provider import PlayerProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -AUDIOMANAGER_STREAM_MUSIC = 3 - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return FullyKioskProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_IP_ADDRESS, - type=ConfigEntryType.STRING, - label="IP-Address (or hostname) of the device running Fully Kiosk/app.", - required=True, - ), - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password to use to connect to the Fully Kiosk API.", - required=True, - ), - ConfigEntry( - key=CONF_PORT, - type=ConfigEntryType.STRING, - default_value="2323", - label="Port to use to connect to the Fully Kiosk API (default is 2323).", - required=True, - category="advanced", - ), - ) - - -class FullyKioskProvider(PlayerProvider): - """Player provider for FullyKiosk based players.""" - - _fully: FullyKiosk - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - # set-up fullykiosk logging - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("fullykiosk").setLevel(logging.DEBUG) - else: - logging.getLogger("fullykiosk").setLevel(self.logger.level + 10) - self._fully = FullyKiosk( - self.mass.http_session, - self.config.get_value(CONF_IP_ADDRESS), - self.config.get_value(CONF_PORT), - self.config.get_value(CONF_PASSWORD), - ) - try: - async with asyncio.timeout(15): - await self._fully.getDeviceInfo() - except Exception as err: - msg = f"Unable to start the FullyKiosk connection ({err!s}" - raise SetupFailedError(msg) from err - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - # Add FullyKiosk device to Player controller. - player_id = self._fully.deviceInfo["deviceID"] - player = self.mass.players.get(player_id, raise_unavailable=False) - address = ( - f"http://{self.config.get_value(CONF_IP_ADDRESS)}:{self.config.get_value(CONF_PORT)}" - ) - if not player: - player = Player( - player_id=player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=self._fully.deviceInfo["deviceName"], - available=True, - powered=False, - device_info=DeviceInfo( - model=self._fully.deviceInfo["deviceModel"], - manufacturer=self._fully.deviceInfo["deviceManufacturer"], - address=address, - ), - supported_features=(PlayerFeature.VOLUME_SET,), - needs_poll=True, - poll_interval=10, - ) - await self.mass.players.register_or_update(player) - self._handle_player_update() - - def _handle_player_update(self) -> None: - """Update FullyKiosk player attributes.""" - player_id = self._fully.deviceInfo["deviceID"] - if not (player := self.mass.players.get(player_id)): - return - player.name = self._fully.deviceInfo["deviceName"] - # player.volume_level = snap_client.volume - for volume_dict in self._fully.deviceInfo.get("audioVolumes", []): - if str(AUDIOMANAGER_STREAM_MUSIC) in volume_dict: - volume = volume_dict[str(AUDIOMANAGER_STREAM_MUSIC)] - player.volume_level = volume - break - current_url = self._fully.deviceInfo.get("soundUrlPlaying") - player.current_item_id = current_url - if not current_url: - player.state = PlayerState.IDLE - player.available = True - self.mass.players.update(player_id) - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - base_entries = await super().get_player_config_entries(player_id) - return ( - *base_entries, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, - ) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - if not (player := self.mass.players.get(player_id, raise_unavailable=False)): - return - await self._fully.setAudioVolume(volume_level, AUDIOMANAGER_STREAM_MUSIC) - player.volume_level = volume_level - self.mass.players.update(player_id) - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - if not (player := self.mass.players.get(player_id, raise_unavailable=False)): - return - await self._fully.stopSound() - player.state = PlayerState.IDLE - self.mass.players.update(player_id) - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - if not (player := self.mass.players.get(player_id)): - return - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): - media.uri = media.uri.replace(".flac", ".mp3") - await self._fully.playSound(media.uri, AUDIOMANAGER_STREAM_MUSIC) - player.current_media = media - player.elapsed_time = 0 - player.elapsed_time_last_updated = time.time() - player.state = PlayerState.PLAYING - self.mass.players.update(player_id) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - try: - async with asyncio.timeout(15): - await self._fully.getDeviceInfo() - self._handle_player_update() - except Exception as err: - msg = f"Unable to start the FullyKiosk connection ({err!s}" - raise PlayerUnavailableError(msg) from err diff --git a/music_assistant/server/providers/fully_kiosk/manifest.json b/music_assistant/server/providers/fully_kiosk/manifest.json deleted file mode 100644 index 1059df8e..00000000 --- a/music_assistant/server/providers/fully_kiosk/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "player", - "domain": "fully_kiosk", - "name": "Fully Kiosk Browser", - "description": "Support for media players from the Fully Kiosk app.", - "codeowners": ["@music-assistant"], - "requirements": ["python-fullykiosk==0.0.14"], - "documentation": "https://music-assistant.io/player-support/fully-kiosk/", - "multi_instance": true, - "builtin": false -} diff --git a/music_assistant/server/providers/hass/__init__.py b/music_assistant/server/providers/hass/__init__.py deleted file mode 100644 index a365b5a5..00000000 --- a/music_assistant/server/providers/hass/__init__.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Home Assistant Plugin for Music Assistant. - -The plugin is the core of all communication to/from Home Assistant and -responsible for maintaining the WebSocket API connection to HA. -Also, the Music Assistant integration within HA will relay its own api -communication over the HA api for more flexibility as well as security. -""" - -from __future__ import annotations - -import asyncio -import logging -from typing import TYPE_CHECKING - -import shortuuid -from hass_client import HomeAssistantClient -from hass_client.exceptions import BaseHassClientError -from hass_client.utils import ( - base_url, - get_auth_url, - get_long_lived_token, - get_token, - get_websocket_url, -) - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import LoginFailed, SetupFailedError -from music_assistant.constants import MASS_LOGO_ONLINE -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models.plugin import PluginProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -DOMAIN = "hass" -CONF_URL = "url" -CONF_AUTH_TOKEN = "token" -CONF_ACTION_AUTH = "auth" -CONF_VERIFY_SSL = "verify_ssl" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return HomeAssistant(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # config flow auth action/step (authenticate button clicked) - if action == CONF_ACTION_AUTH: - hass_url = values[CONF_URL] - async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: - client_id = base_url(auth_helper.callback_url) - auth_url = get_auth_url( - hass_url, - auth_helper.callback_url, - client_id=client_id, - state=values["session_id"], - ) - result = await auth_helper.authenticate(auth_url) - if result["state"] != values["session_id"]: - msg = "session id mismatch" - raise LoginFailed(msg) - # get access token after auth was a success - token_details = await get_token(hass_url, result["code"], client_id=client_id) - # register for a long lived token - long_lived_token = await get_long_lived_token( - hass_url, - token_details["access_token"], - client_name=f"Music Assistant {shortuuid.random(6)}", - client_icon=MASS_LOGO_ONLINE, - lifespan=365 * 2, - ) - # set the retrieved token on the values object to pass along - values[CONF_AUTH_TOKEN] = long_lived_token - - if mass.running_as_hass_addon: - # on supervisor, we use the internal url - # token set to None for auto retrieval - return ( - ConfigEntry( - key=CONF_URL, - type=ConfigEntryType.STRING, - label=CONF_URL, - required=True, - default_value="http://supervisor/core/api", - value="http://supervisor/core/api", - hidden=True, - ), - ConfigEntry( - key=CONF_AUTH_TOKEN, - type=ConfigEntryType.STRING, - label=CONF_AUTH_TOKEN, - required=False, - default_value=None, - value=None, - hidden=True, - ), - ConfigEntry( - key=CONF_VERIFY_SSL, - type=ConfigEntryType.BOOLEAN, - label=CONF_VERIFY_SSL, - required=False, - default_value=False, - hidden=True, - ), - ) - # manual configuration - return ( - ConfigEntry( - key=CONF_URL, - type=ConfigEntryType.STRING, - label="URL", - required=True, - description="URL to your Home Assistant instance (e.g. http://192.168.1.1:8123)", - value=values.get(CONF_URL) if values else None, - ), - ConfigEntry( - key=CONF_ACTION_AUTH, - type=ConfigEntryType.ACTION, - label="(re)Authenticate Home Assistant", - description="Authenticate to your home assistant " - "instance and generate the long lived token.", - action=CONF_ACTION_AUTH, - depends_on=CONF_URL, - required=False, - ), - ConfigEntry( - key=CONF_AUTH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Authentication token for HomeAssistant", - description="You can either paste a Long Lived Token here manually or use the " - "'authenticate' button to generate a token for you with logging in.", - depends_on=CONF_URL, - value=values.get(CONF_AUTH_TOKEN) if values else None, - category="advanced", - ), - ConfigEntry( - key=CONF_VERIFY_SSL, - type=ConfigEntryType.BOOLEAN, - label="Verify SSL", - required=False, - description="Whether or not to verify the certificate of SSL/TLS connections.", - category="advanced", - default_value=True, - ), - ) - - -class HomeAssistant(PluginProvider): - """Home Assistant Plugin for Music Assistant.""" - - hass: HomeAssistantClient - _listen_task: asyncio.Task | None = None - - async def handle_async_init(self) -> None: - """Handle async initialization of the plugin.""" - url = get_websocket_url(self.config.get_value(CONF_URL)) - token = self.config.get_value(CONF_AUTH_TOKEN) - logging.getLogger("hass_client").setLevel(self.logger.level + 10) - self.hass = HomeAssistantClient(url, token, self.mass.http_session) - try: - await self.hass.connect(ssl=bool(self.config.get_value(CONF_VERIFY_SSL))) - except BaseHassClientError as err: - err_msg = str(err) or err.__class__.__name__ - raise SetupFailedError(err_msg) from err - self._listen_task = self.mass.create_task(self._hass_listener()) - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - if self._listen_task and not self._listen_task.done(): - self._listen_task.cancel() - await self.hass.disconnect() - - async def _hass_listener(self) -> None: - """Start listening on the HA websockets.""" - try: - # start listening will block until the connection is lost/closed - await self.hass.start_listening() - except BaseHassClientError as err: - self.logger.warning("Connection to HA lost due to error: %s", err) - self.logger.info("Connection to HA lost. Connection will be automatically retried later.") - # schedule a reload of the provider - self.mass.call_later(5, self.mass.load_provider, self.instance_id, allow_retry=True) diff --git a/music_assistant/server/providers/hass/icon.svg b/music_assistant/server/providers/hass/icon.svg deleted file mode 100644 index 73037fee..00000000 --- a/music_assistant/server/providers/hass/icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/music_assistant/server/providers/hass/manifest.json b/music_assistant/server/providers/hass/manifest.json deleted file mode 100644 index 4fee6af8..00000000 --- a/music_assistant/server/providers/hass/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "plugin", - "domain": "hass", - "name": "Home Assistant", - "description": "Connect Music Assistant to Home Assistant.", - "codeowners": ["@music-assistant"], - "documentation": "", - "multi_instance": false, - "builtin": false, - "icon": "md:webhook", - "requirements": ["hass-client==1.2.0"] -} diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/server/providers/hass_players/__init__.py deleted file mode 100644 index 2ed93adf..00000000 --- a/music_assistant/server/providers/hass_players/__init__.py +++ /dev/null @@ -1,489 +0,0 @@ -""" -Home Assistant PlayerProvider for Music Assistant. - -Allows using media_player entities in HA to be used as players in MA. -Requires the Home Assistant Plugin. -""" - -from __future__ import annotations - -import time -from enum import IntFlag -from typing import TYPE_CHECKING, Any - -from hass_client.exceptions import FailedCommand - -from music_assistant.common.helpers.datetime import from_iso_string -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_ENABLE_ICY_METADATA, - CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_HTTP_PROFILE, - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import SetupFailedError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.hass import DOMAIN as HASS_DOMAIN - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from hass_client.models import CompressedState, EntityStateEvent - from hass_client.models import Device as HassDevice - from hass_client.models import Entity as HassEntity - from hass_client.models import State as HassState - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - from music_assistant.server.providers.hass import HomeAssistant as HomeAssistantProvider - -CONF_PLAYERS = "players" - -StateMap = { - "playing": PlayerState.PLAYING, - "paused": PlayerState.PAUSED, - "buffering": PlayerState.PLAYING, - "idle": PlayerState.IDLE, - "off": PlayerState.IDLE, - "standby": PlayerState.IDLE, - "unknown": PlayerState.IDLE, - "unavailable": PlayerState.IDLE, -} - - -class MediaPlayerEntityFeature(IntFlag): - """Supported features of the media player entity.""" - - PAUSE = 1 - SEEK = 2 - VOLUME_SET = 4 - VOLUME_MUTE = 8 - PREVIOUS_TRACK = 16 - NEXT_TRACK = 32 - - TURN_ON = 128 - TURN_OFF = 256 - PLAY_MEDIA = 512 - VOLUME_STEP = 1024 - SELECT_SOURCE = 2048 - STOP = 4096 - CLEAR_PLAYLIST = 8192 - PLAY = 16384 - SHUFFLE_SET = 32768 - SELECT_SOUND_MODE = 65536 - BROWSE_MEDIA = 131072 - REPEAT_SET = 262144 - GROUPING = 524288 - MEDIA_ANNOUNCE = 1048576 - MEDIA_ENQUEUE = 2097152 - - -CONF_ENFORCE_MP3 = "enforce_mp3" - - -PLAYER_CONFIG_ENTRIES = ( - CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, - CONF_ENTRY_HTTP_PROFILE, - CONF_ENTRY_ENABLE_ICY_METADATA, -) - - -async def _get_hass_media_players( - hass_prov: HomeAssistantProvider, -) -> AsyncGenerator[HassState, None]: - """Return all HA state objects for (valid) media_player entities.""" - for state in await hass_prov.hass.get_states(): - if not state["entity_id"].startswith("media_player"): - continue - if "mass_player_id" in state["attributes"]: - # filter out mass players - continue - if "friendly_name" not in state["attributes"]: - # filter out invalid/unavailable players - continue - supported_features = MediaPlayerEntityFeature(state["attributes"]["supported_features"]) - if MediaPlayerEntityFeature.PLAY_MEDIA not in supported_features: - continue - yield state - - -async def _get_hass_media_player( - hass_prov: HomeAssistantProvider, entity_id: str -) -> HassState | None: - """Return Hass state object for a single media_player entity.""" - for state in await hass_prov.hass.get_states(): - if state["entity_id"] == entity_id: - return state - return None - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - hass_prov: HomeAssistantProvider = mass.get_provider(HASS_DOMAIN) - if not hass_prov: - msg = "The Home Assistant Plugin needs to be set-up first" - raise SetupFailedError(msg) - prov = HomeAssistantPlayers(mass, manifest, config) - prov.hass_prov = hass_prov - return prov - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - hass_prov: HomeAssistantProvider = mass.get_provider(HASS_DOMAIN) - player_entities: list[ConfigValueOption] = [] - if hass_prov and hass_prov.hass.connected: - async for state in _get_hass_media_players(hass_prov): - name = f'{state["attributes"]["friendly_name"]} ({state["entity_id"]})' - player_entities.append(ConfigValueOption(name, state["entity_id"])) - return ( - ConfigEntry( - key=CONF_PLAYERS, - type=ConfigEntryType.STRING, - label="Player entities", - required=True, - options=tuple(player_entities), - multi_value=True, - description="Specify which HA media_player entity id's you " - "like to import as players in Music Assistant.", - ), - ) - - -class HomeAssistantPlayers(PlayerProvider): - """Home Assistant PlayerProvider for Music Assistant.""" - - hass_prov: HomeAssistantProvider - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - await super().loaded_in_mass() - player_ids: list[str] = self.config.get_value(CONF_PLAYERS) - # prefetch the device- and entity registry - device_registry = {x["id"]: x for x in await self.hass_prov.hass.get_device_registry()} - entity_registry = { - x["entity_id"]: x for x in await self.hass_prov.hass.get_entity_registry() - } - # setup players from hass entities - async for state in _get_hass_media_players(self.hass_prov): - if state["entity_id"] not in player_ids: - continue - await self._setup_player(state, entity_registry, device_registry) - # register for entity state updates - await self.hass_prov.hass.subscribe_entities(self._on_entity_state_update, player_ids) - # remove any leftover players (after reconfigure of players) - for player in self.players: - if player.player_id not in player_ids: - self.mass.players.remove(player.player_id) - - async def get_player_config_entries( - self, - player_id: str, - ) -> tuple[ConfigEntry, ...]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - entries = await super().get_player_config_entries(player_id) - entries = entries + PLAYER_CONFIG_ENTRIES - if hass_state := await _get_hass_media_player(self.hass_prov, player_id): - hass_supported_features = MediaPlayerEntityFeature( - hass_state["attributes"]["supported_features"] - ) - if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in hass_supported_features: - entries += (CONF_ENTRY_FLOW_MODE_ENFORCED,) - - return entries - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player. - - - player_id: player_id of the player to handle the command. - """ - try: - await self.hass_prov.hass.call_service( - domain="media_player", service="media_stop", target={"entity_id": player_id} - ) - except FailedCommand as exc: - # some HA players do not support STOP - if "does not support this service" not in str(exc): - raise - if player := self.mass.players.get(player_id): - if PlayerFeature.PAUSE in player.supported_features: - await self.cmd_pause(player_id) - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY (unpause) command to given player. - - - player_id: player_id of the player to handle the command. - """ - await self.hass_prov.hass.call_service( - domain="media_player", service="media_play", target={"entity_id": player_id} - ) - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player. - - - player_id: player_id of the player to handle the command. - """ - await self.hass_prov.hass.call_service( - domain="media_player", - service="media_pause", - target={"entity_id": player_id}, - ) - - async def play_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle PLAY MEDIA on given player.""" - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): - media.uri = media.uri.replace(".flac", ".mp3") - await self.hass_prov.hass.call_service( - domain="media_player", - service="play_media", - service_data={ - "media_content_id": media.uri, - "media_content_type": "music", - "enqueue": "replace", - "extra": { - "metadata": { - "title": media.title, - "artist": media.artist, - "metadataType": 3, - "album": media.album, - "albumName": media.album, - "duration": media.duration, - "images": [{"url": media.image_url}] if media.image_url else None, - "imageUrl": media.image_url, - } - }, - }, - target={"entity_id": player_id}, - ) - # optimistically set the elapsed_time as some HA players do not report this - if player := self.mass.players.get(player_id): - player.elapsed_time = 0 - player.elapsed_time_last_updated = time.time() - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next queue item on the player.""" - if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True): - media.uri = media.uri.replace(".flac", ".mp3") - await self.hass_prov.hass.call_service( - domain="media_player", - service="play_media", - service_data={ - "media_content_id": media.uri, - "media_content_type": "music", - "enqueue": "next", - }, - target={"entity_id": player_id}, - ) - - async def cmd_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player. - - - player_id: player_id of the player to handle the command. - - powered: bool if player should be powered on or off. - """ - await self.hass_prov.hass.call_service( - domain="media_player", - service="turn_on" if powered else "turn_off", - target={"entity_id": player_id}, - ) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player. - - - player_id: player_id of the player to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - await self.hass_prov.hass.call_service( - domain="media_player", - service="volume_set", - service_data={"volume_level": volume_level / 100}, - target={"entity_id": player_id}, - ) - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player. - - - player_id: player_id of the player to handle the command. - - muted: bool if player should be muted. - """ - await self.hass_prov.hass.call_service( - domain="media_player", - service="volume_mute", - service_data={"is_volume_muted": muted}, - target={"entity_id": player_id}, - ) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - # NOTE: not in use yet, as we do not support syncgroups in MA for HA players - await self.hass_prov.hass.call_service( - domain="media_player", - service="join", - service_data={"group_members": [player_id]}, - target={"entity_id": target_player}, - ) - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - # NOTE: not in use yet, as we do not support syncgroups in MA for HA players - await self.hass_prov.hass.call_service( - domain="media_player", - service="unjoin", - target={"entity_id": player_id}, - ) - - async def _setup_player( - self, - state: HassState, - entity_registry: dict[str, HassEntity], - device_registry: dict[str, HassDevice], - ) -> None: - """Handle setup of a Player from an hass entity.""" - hass_device: HassDevice | None = None - if entity_registry_entry := entity_registry.get(state["entity_id"]): - hass_device = device_registry.get(entity_registry_entry["device_id"]) - hass_supported_features = MediaPlayerEntityFeature( - state["attributes"]["supported_features"] - ) - supported_features: list[PlayerFeature] = [] - if MediaPlayerEntityFeature.PAUSE in hass_supported_features: - supported_features.append(PlayerFeature.PAUSE) - if MediaPlayerEntityFeature.VOLUME_SET in hass_supported_features: - supported_features.append(PlayerFeature.VOLUME_SET) - if MediaPlayerEntityFeature.VOLUME_MUTE in hass_supported_features: - supported_features.append(PlayerFeature.VOLUME_MUTE) - if MediaPlayerEntityFeature.MEDIA_ENQUEUE in hass_supported_features: - supported_features.append(PlayerFeature.ENQUEUE) - if ( - MediaPlayerEntityFeature.TURN_ON in hass_supported_features - and MediaPlayerEntityFeature.TURN_OFF in hass_supported_features - ): - supported_features.append(PlayerFeature.POWER) - player = Player( - player_id=state["entity_id"], - provider=self.instance_id, - type=PlayerType.PLAYER, - name=state["attributes"]["friendly_name"], - available=state["state"] not in ("unavailable", "unknown"), - powered=state["state"] not in ("unavailable", "unknown", "standby", "off"), - device_info=DeviceInfo( - model=hass_device["model"] if hass_device else "Unknown model", - manufacturer=( - hass_device["manufacturer"] if hass_device else "Unknown Manufacturer" - ), - ), - supported_features=tuple(supported_features), - state=StateMap.get(state["state"], PlayerState.IDLE), - ) - self._update_player_attributes(player, state["attributes"]) - await self.mass.players.register_or_update(player) - - def _on_entity_state_update(self, event: EntityStateEvent) -> None: - """Handle Entity State event.""" - - def update_player_from_state_msg(entity_id: str, state: CompressedState) -> None: - """Handle updating MA player with updated info in a HA CompressedState.""" - player = self.mass.players.get(entity_id) - if player is None: - # edge case - one of our subscribed entities was not available at startup - # and now came available - we should still set it up - player_ids: list[str] = self.config.get_value(CONF_PLAYERS) - if entity_id not in player_ids: - return # should not happen, but guard just in case - self.mass.create_task(self._late_add_player(entity_id)) - return - if "s" in state: - player.state = StateMap.get(state["s"], PlayerState.IDLE) - player.powered = state["s"] not in ( - "unavailable", - "unknown", - "standby", - "off", - ) - if "a" in state: - self._update_player_attributes(player, state["a"]) - self.mass.players.update(entity_id) - - if entity_additions := event.get("a"): - for entity_id, state in entity_additions.items(): - update_player_from_state_msg(entity_id, state) - if entity_changes := event.get("c"): - for entity_id, state_diff in entity_changes.items(): - if "+" not in state_diff: - continue - update_player_from_state_msg(entity_id, state_diff["+"]) - - def _update_player_attributes(self, player: Player, attributes: dict[str, Any]) -> None: - """Update Player attributes from HA state attributes.""" - for key, value in attributes.items(): - if key == "media_position": - player.elapsed_time = value - if key == "media_position_updated_at": - player.elapsed_time_last_updated = from_iso_string(value).timestamp() - if key == "volume_level": - player.volume_level = int(value * 100) - if key == "volume_muted": - player.volume_muted = value - if key == "media_content_id": - player.current_item_id = value - if key == "group_members": - if value and value[0] == player.player_id: - player.group_childs = value - player.synced_to = None - elif value and value[0] != player.player_id: - player.group_childs = set() - player.synced_to = value[0] - else: - player.group_childs = set() - player.synced_to = None - - async def _late_add_player(self, entity_id: str) -> None: - """Handle setup of Player from HA entity that became available after startup.""" - # prefetch the device- and entity registry - device_registry = {x["id"]: x for x in await self.hass_prov.hass.get_device_registry()} - entity_registry = { - x["entity_id"]: x for x in await self.hass_prov.hass.get_entity_registry() - } - async for state in _get_hass_media_players(self.hass_prov): - if state["entity_id"] != entity_id: - continue - await self._setup_player(state, entity_registry, device_registry) diff --git a/music_assistant/server/providers/hass_players/icon.svg b/music_assistant/server/providers/hass_players/icon.svg deleted file mode 100644 index 73037fee..00000000 --- a/music_assistant/server/providers/hass_players/icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/music_assistant/server/providers/hass_players/manifest.json b/music_assistant/server/providers/hass_players/manifest.json deleted file mode 100644 index 3c0fae3e..00000000 --- a/music_assistant/server/providers/hass_players/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "player", - "domain": "hass_players", - "name": "Home Assistant MediaPlayers", - "description": "Use (supported) Home Assistant media players as players in Music Assistant.", - "codeowners": ["@music-assistant"], - "documentation": "https://music-assistant.io/player-support/ha/", - "multi_instance": false, - "builtin": false, - "icon": "md:webhook", - "depends_on": "hass", - "requirements": [] -} diff --git a/music_assistant/server/providers/jellyfin/__init__.py b/music_assistant/server/providers/jellyfin/__init__.py deleted file mode 100644 index b8abc700..00000000 --- a/music_assistant/server/providers/jellyfin/__init__.py +++ /dev/null @@ -1,505 +0,0 @@ -"""Jellyfin support for MusicAssistant.""" - -from __future__ import annotations - -import mimetypes -import socket -import uuid -from asyncio import TaskGroup -from collections.abc import AsyncGenerator - -from aiojellyfin import MediaLibrary as JellyMediaLibrary -from aiojellyfin import NotFound, SessionConfiguration, authenticate_by_name -from aiojellyfin import Track as JellyTrack - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import UNKNOWN_ARTIST_ID_MBID -from music_assistant.server.models import ProviderInstanceType -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.providers.jellyfin.parsers import ( - parse_album, - parse_artist, - parse_playlist, - parse_track, -) -from music_assistant.server.server import MusicAssistant - -from .const import ( - ALBUM_FIELDS, - ARTIST_FIELDS, - CLIENT_VERSION, - ITEM_KEY_COLLECTION_TYPE, - ITEM_KEY_ID, - ITEM_KEY_MEDIA_CHANNELS, - ITEM_KEY_MEDIA_CODEC, - ITEM_KEY_MEDIA_SOURCES, - ITEM_KEY_MEDIA_STREAMS, - ITEM_KEY_NAME, - ITEM_KEY_RUNTIME_TICKS, - SUPPORTED_CONTAINER_FORMATS, - TRACK_FIELDS, - UNKNOWN_ARTIST_MAPPING, - USER_APP_NAME, -) - -CONF_URL = "url" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" -CONF_VERIFY_SSL = "verify_ssl" -FAKE_ARTIST_PREFIX = "_fake://" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return JellyfinProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # config flow auth action/step (authenticate button clicked) - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_URL, - type=ConfigEntryType.STRING, - label="Server", - required=True, - description="The url of the Jellyfin server to connect to.", - ), - ConfigEntry( - key=CONF_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - description="The username to authenticate to the remote server." - "the remote host, For example 'media'.", - ), - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password", - required=False, - description="The password to authenticate to the remote server.", - ), - ConfigEntry( - key=CONF_VERIFY_SSL, - type=ConfigEntryType.BOOLEAN, - label="Verify SSL", - required=False, - description="Whether or not to verify the certificate of SSL/TLS connections.", - category="advanced", - default_value=True, - ), - ) - - -class JellyfinProvider(MusicProvider): - """Provider for a jellyfin music library.""" - - async def handle_async_init(self) -> None: - """Initialize provider(instance) with given configuration.""" - session_config = SessionConfiguration( - session=self.mass.http_session, - url=str(self.config.get_value(CONF_URL)), - verify_ssl=bool(self.config.get_value(CONF_VERIFY_SSL)), - app_name=USER_APP_NAME, - app_version=CLIENT_VERSION, - device_name=socket.gethostname(), - device_id=str(uuid.uuid4()), - ) - - try: - self._client = await authenticate_by_name( - session_config, - str(self.config.get_value(CONF_USERNAME)), - str(self.config.get_value(CONF_PASSWORD)), - ) - except Exception as err: - raise LoginFailed(f"Authentication failed: {err}") from err - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return a list of supported features.""" - return ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.SIMILAR_TRACKS, - ) - - @property - def is_streaming_provider(self) -> bool: - """Return True if the provider is a streaming provider.""" - return False - - async def _search_track(self, search_query: str, limit: int) -> list[Track]: - resultset = ( - await self._client.tracks.search_term(search_query) - .limit(limit) - .enable_userdata() - .fields(*TRACK_FIELDS) - .request() - ) - tracks = [] - for item in resultset["Items"]: - tracks.append(parse_track(self.logger, self.instance_id, self._client, item)) - return tracks - - async def _search_album(self, search_query: str, limit: int) -> list[Album]: - if "-" in search_query: - searchterms = search_query.split(" - ") - albumname = searchterms[1] - else: - albumname = search_query - resultset = ( - await self._client.albums.search_term(albumname) - .limit(limit) - .enable_userdata() - .fields(*ALBUM_FIELDS) - .request() - ) - albums = [] - for item in resultset["Items"]: - albums.append(parse_album(self.logger, self.instance_id, self._client, item)) - return albums - - async def _search_artist(self, search_query: str, limit: int) -> list[Artist]: - resultset = ( - await self._client.artists.search_term(search_query) - .limit(limit) - .enable_userdata() - .fields(*ARTIST_FIELDS) - .request() - ) - artists = [] - for item in resultset["Items"]: - artists.append(parse_artist(self.logger, self.instance_id, self._client, item)) - return artists - - async def _search_playlist(self, search_query: str, limit: int) -> list[Playlist]: - resultset = ( - await self._client.playlists.search_term(search_query) - .limit(limit) - .enable_userdata() - .request() - ) - playlists = [] - for item in resultset["Items"]: - playlists.append(parse_playlist(self.instance_id, self._client, item)) - return playlists - - async def search( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 20, - ) -> SearchResults: - """Perform search on the plex library. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - artists = None - albums = None - tracks = None - playlists = None - - async with TaskGroup() as tg: - if MediaType.ARTIST in media_types: - artists = tg.create_task(self._search_artist(search_query, limit)) - if MediaType.ALBUM in media_types: - albums = tg.create_task(self._search_album(search_query, limit)) - if MediaType.TRACK in media_types: - tracks = tg.create_task(self._search_track(search_query, limit)) - if MediaType.PLAYLIST in media_types: - playlists = tg.create_task(self._search_playlist(search_query, limit)) - - search_results = SearchResults() - - if artists: - search_results.artists = artists.result() - if albums: - search_results.albums = albums.result() - if tracks: - search_results.tracks = tracks.result() - if playlists: - search_results.playlists = playlists.result() - - return search_results - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Jellyfin Music.""" - jellyfin_libraries = await self._get_music_libraries() - for jellyfin_library in jellyfin_libraries: - stream = ( - self._client.artists.parent(jellyfin_library[ITEM_KEY_ID]) - .enable_userdata() - .fields(*ARTIST_FIELDS) - .stream(100) - ) - async for artist in stream: - yield parse_artist(self.logger, self.instance_id, self._client, artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Jellyfin Music.""" - jellyfin_libraries = await self._get_music_libraries() - for jellyfin_library in jellyfin_libraries: - stream = ( - self._client.albums.parent(jellyfin_library[ITEM_KEY_ID]) - .enable_userdata() - .fields(*ALBUM_FIELDS) - .stream(100) - ) - async for album in stream: - yield parse_album(self.logger, self.instance_id, self._client, album) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Jellyfin Music.""" - jellyfin_libraries = await self._get_music_libraries() - for jellyfin_library in jellyfin_libraries: - stream = ( - self._client.tracks.parent(jellyfin_library[ITEM_KEY_ID]) - .enable_userdata() - .fields(*TRACK_FIELDS) - .stream(100) - ) - async for track in stream: - if not len(track[ITEM_KEY_MEDIA_STREAMS]): - self.logger.warning( - "Invalid track %s: Does not have any media streams", track[ITEM_KEY_NAME] - ) - continue - yield parse_track(self.logger, self.instance_id, self._client, track) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from the provider.""" - playlist_libraries = await self._get_playlists() - for playlist_library in playlist_libraries: - stream = ( - self._client.playlists.parent(playlist_library[ITEM_KEY_ID]) - .enable_userdata() - .stream(100) - ) - async for playlist in stream: - if "MediaType" in playlist: # Only jellyfin has this property - if playlist["MediaType"] == "Audio": - yield parse_playlist(self.instance_id, self._client, playlist) - else: # emby playlists are only audio type - yield parse_playlist(self.instance_id, self._client, playlist) - - async def get_album(self, prov_album_id: str) -> Album: - """Get full album details by id.""" - try: - album = await self._client.get_album(prov_album_id) - except NotFound: - raise MediaNotFoundError(f"Item {prov_album_id} not found") - return parse_album(self.logger, self.instance_id, self._client, album) - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get album tracks for given album id.""" - jellyfin_album_tracks = ( - await self._client.tracks.parent(prov_album_id) - .enable_userdata() - .fields(*TRACK_FIELDS) - .request() - ) - return [ - parse_track(self.logger, self.instance_id, self._client, jellyfin_album_track) - for jellyfin_album_track in jellyfin_album_tracks["Items"] - ] - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - if prov_artist_id == UNKNOWN_ARTIST_MAPPING.item_id: - artist = Artist( - item_id=UNKNOWN_ARTIST_MAPPING.item_id, - name=UNKNOWN_ARTIST_MAPPING.name, - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=UNKNOWN_ARTIST_MAPPING.item_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - artist.mbid = UNKNOWN_ARTIST_ID_MBID - return artist - - try: - jellyfin_artist = await self._client.get_artist(prov_artist_id) - except NotFound: - raise MediaNotFoundError(f"Item {prov_artist_id} not found") - return parse_artist(self.logger, self.instance_id, self._client, jellyfin_artist) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - try: - track = await self._client.get_track(prov_track_id) - except NotFound: - raise MediaNotFoundError(f"Item {prov_track_id} not found") - return parse_track(self.logger, self.instance_id, self._client, track) - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id.""" - try: - playlist = await self._client.get_playlist(prov_playlist_id) - except NotFound: - raise MediaNotFoundError(f"Item {prov_playlist_id} not found") - return parse_playlist(self.instance_id, self._client, playlist) - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # paging not supported, we always return the whole list at once - return [] - # TODO: Does Jellyfin support paging here? - jellyfin_playlist = await self._client.get_playlist(prov_playlist_id) - playlist_items = ( - await self._client.tracks.parent(jellyfin_playlist[ITEM_KEY_ID]) - .enable_userdata() - .fields(*TRACK_FIELDS) - .request() - ) - for index, jellyfin_track in enumerate(playlist_items["Items"], 1): - try: - if track := parse_track( - self.logger, self.instance_id, self._client, jellyfin_track - ): - if not track.position: - track.position = index - result.append(track) - except (KeyError, ValueError) as err: - self.logger.error( - "Skipping track %s: %s", jellyfin_track.get(ITEM_KEY_NAME, index), str(err) - ) - return result - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get a list of albums for the given artist.""" - if not prov_artist_id.startswith(FAKE_ARTIST_PREFIX): - return [] - albums = ( - await self._client.albums.parent(prov_artist_id) - .fields(*ALBUM_FIELDS) - .enable_userdata() - .request() - ) - return [ - parse_album(self.logger, self.instance_id, self._client, album) - for album in albums["Items"] - ] - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - jellyfin_track = await self._client.get_track(item_id) - mimetype = self._media_mime_type(jellyfin_track) - media_stream = jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0] - url = self._client.audio_url(jellyfin_track[ITEM_KEY_ID], SUPPORTED_CONTAINER_FORMATS) - if ITEM_KEY_MEDIA_CODEC in media_stream: - content_type = ContentType.try_parse(media_stream[ITEM_KEY_MEDIA_CODEC]) - else: - content_type = ContentType.try_parse(mimetype) if mimetype else ContentType.UNKNOWN - return StreamDetails( - item_id=jellyfin_track[ITEM_KEY_ID], - provider=self.instance_id, - audio_format=AudioFormat( - content_type=content_type, - channels=jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0][ITEM_KEY_MEDIA_CHANNELS], - ), - stream_type=StreamType.HTTP, - duration=int( - jellyfin_track[ITEM_KEY_RUNTIME_TICKS] / 10000000 - ), # 10000000 ticks per millisecond) - path=url, - ) - - async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - resp = await self._client.get_similar_tracks( - prov_track_id, limit=limit, fields=TRACK_FIELDS - ) - return [ - parse_track(self.logger, self.instance_id, self._client, track) - for track in resp["Items"] - ] - - async def _get_music_libraries(self) -> list[JellyMediaLibrary]: - """Return all supported libraries a user has access to.""" - response = await self._client.get_media_folders() - libraries = response["Items"] - result = [] - for library in libraries: - if ITEM_KEY_COLLECTION_TYPE in library and library[ITEM_KEY_COLLECTION_TYPE] in "music": - result.append(library) - return result - - async def _get_playlists(self) -> list[JellyMediaLibrary]: - """Return all supported libraries a user has access to.""" - response = await self._client.get_media_folders() - libraries = response["Items"] - result = [] - for library in libraries: - if ( - ITEM_KEY_COLLECTION_TYPE in library - and library[ITEM_KEY_COLLECTION_TYPE] in "playlists" - ): - result.append(library) - return result - - def _media_mime_type(self, media_item: JellyTrack) -> str | None: - """Return the mime type of a media item.""" - if not media_item.get(ITEM_KEY_MEDIA_SOURCES): - return None - - media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] - - if "Path" not in media_source: - return None - - path = media_source["Path"] - mime_type, _ = mimetypes.guess_type(path) - - return mime_type diff --git a/music_assistant/server/providers/jellyfin/const.py b/music_assistant/server/providers/jellyfin/const.py deleted file mode 100644 index 2bbfc9a9..00000000 --- a/music_assistant/server/providers/jellyfin/const.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Constants for the Jellyfin integration.""" - -from typing import Final - -from aiojellyfin import ImageType as JellyImageType -from aiojellyfin import ItemFields - -from music_assistant.common.models.enums import ImageType, MediaType -from music_assistant.common.models.media_items import ItemMapping -from music_assistant.constants import UNKNOWN_ARTIST - -DOMAIN: Final = "jellyfin" - -CLIENT_VERSION: Final = "0.1" - -COLLECTION_TYPE_MOVIES: Final = "movies" -COLLECTION_TYPE_MUSIC: Final = "music" -COLLECTION_TYPE_TVSHOWS: Final = "tvshows" - -CONF_CLIENT_DEVICE_ID: Final = "client_device_id" - -DEFAULT_NAME: Final = "Jellyfin" - -ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType" -ITEM_KEY_ID: Final = "Id" -ITEM_KEY_IMAGE_TAGS: Final = "ImageTags" -ITEM_KEY_INDEX_NUMBER: Final = "IndexNumber" -ITEM_KEY_MEDIA_SOURCES: Final = "MediaSources" -ITEM_KEY_MEDIA_TYPE: Final = "MediaType" -ITEM_KEY_MEDIA_STREAMS: Final = "MediaStreams" -ITEM_KEY_MEDIA_CHANNELS: Final = "Channels" -ITEM_KEY_MEDIA_CODEC: Final = "Codec" -ITEM_KEY_NAME: Final = "Name" -ITEM_KEY_PROVIDER_IDS: Final = "ProviderIds" -ITEM_KEY_PRODUCTION_YEAR: Final = "ProductionYear" -ITEM_KEY_OVERVIEW: Final = "Overview" -ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP: Final = "MusicBrainzReleaseGroup" -ITEM_KEY_MUSICBRAINZ_ARTIST: Final = "MusicBrainzArtist" -ITEM_KEY_MUSICBRAINZ_ALBUM: Final = "MusicBrainzAlbum" -ITEM_KEY_MUSICBRAINZ_TRACK: Final = "MusicBrainzTrack" -ITEM_KEY_SORT_NAME: Final = "SortName" -ITEM_KEY_ALBUM_ARTIST: Final = "AlbumArtist" -ITEM_KEY_ALBUM_ARTISTS: Final = "AlbumArtists" -ITEM_KEY_ALBUM: Final = "Album" -ITEM_KEY_ALBUM_ID: Final = "AlbumId" -ITEM_KEY_ARTIST_ITEMS: Final = "ArtistItems" -ITEM_KEY_CAN_DOWNLOAD: Final = "CanDownload" -ITEM_KEY_PARENT_INDEX_NUM: Final = "ParentIndexNumber" -ITEM_KEY_RUNTIME_TICKS: Final = "RunTimeTicks" -ITEM_KEY_USER_DATA: Final = "UserData" - -ITEM_TYPE_AUDIO: Final = "Audio" -ITEM_TYPE_LIBRARY: Final = "CollectionFolder" - -USER_DATA_KEY_IS_FAVORITE: Final = "IsFavorite" - -MAX_IMAGE_WIDTH: Final = 500 -MAX_STREAMING_BITRATE: Final = "140000000" - -MEDIA_SOURCE_KEY_PATH: Final = "Path" - -MEDIA_TYPE_AUDIO: Final = "Audio" -MEDIA_TYPE_NONE: Final = "" - -SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC] - -SUPPORTED_CONTAINER_FORMATS: Final = "ogg,flac,mp3,aac,mpeg,alac,wav,aiff,wma,m4a,m4b,dsf,opus,wv" - -PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO] - -ARTIST_FIELDS: Final = [ - ItemFields.Overview, - ItemFields.ProviderIds, - ItemFields.SortName, -] -ALBUM_FIELDS: Final = [ - ItemFields.Overview, - ItemFields.ProviderIds, - ItemFields.SortName, -] -TRACK_FIELDS: Final = [ - ItemFields.ProviderIds, - ItemFields.CanDownload, - ItemFields.SortName, - ItemFields.MediaSources, - ItemFields.MediaStreams, -] - -USER_APP_NAME: Final = "Music Assistant" -USER_AGENT: Final = "Music-Assistant-1.0" - -UNKNOWN_ARTIST_MAPPING: Final = ItemMapping( - media_type=MediaType.ARTIST, item_id=UNKNOWN_ARTIST, provider=DOMAIN, name=UNKNOWN_ARTIST -) - -MEDIA_IMAGE_TYPES: Final = { - JellyImageType.Primary: ImageType.THUMB, - JellyImageType.Logo: ImageType.LOGO, -} diff --git a/music_assistant/server/providers/jellyfin/icon.svg b/music_assistant/server/providers/jellyfin/icon.svg deleted file mode 100644 index 87a6dd39..00000000 --- a/music_assistant/server/providers/jellyfin/icon.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - diff --git a/music_assistant/server/providers/jellyfin/manifest.json b/music_assistant/server/providers/jellyfin/manifest.json deleted file mode 100644 index cf9cca56..00000000 --- a/music_assistant/server/providers/jellyfin/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "jellyfin", - "name": "Jellyfin Media Server Library", - "description": "Support for the Jellyfin streaming provider in Music Assistant.", - "codeowners": ["@lokiberra", "@Jc2k"], - "requirements": ["aiojellyfin==0.10.1"], - "documentation": "https://music-assistant.io/music-providers/jellyfin/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/jellyfin/parsers.py b/music_assistant/server/providers/jellyfin/parsers.py deleted file mode 100644 index 1d022ce6..00000000 --- a/music_assistant/server/providers/jellyfin/parsers.py +++ /dev/null @@ -1,306 +0,0 @@ -"""Parse Jellyfin metadata into Music Assistant models.""" - -from __future__ import annotations - -import logging -from logging import Logger -from typing import TYPE_CHECKING - -from aiojellyfin import ImageType as JellyImageType - -from music_assistant.common.models.enums import ContentType, ExternalID, ImageType, MediaType -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - ItemMapping, - MediaItemImage, - Playlist, - ProviderMapping, - Track, - UniqueList, -) - -from .const import ( - DOMAIN, - ITEM_KEY_ALBUM, - ITEM_KEY_ALBUM_ARTIST, - ITEM_KEY_ALBUM_ARTISTS, - ITEM_KEY_ALBUM_ID, - ITEM_KEY_ARTIST_ITEMS, - ITEM_KEY_CAN_DOWNLOAD, - ITEM_KEY_ID, - ITEM_KEY_IMAGE_TAGS, - ITEM_KEY_MEDIA_CODEC, - ITEM_KEY_MEDIA_STREAMS, - ITEM_KEY_MUSICBRAINZ_ALBUM, - ITEM_KEY_MUSICBRAINZ_ARTIST, - ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP, - ITEM_KEY_MUSICBRAINZ_TRACK, - ITEM_KEY_NAME, - ITEM_KEY_OVERVIEW, - ITEM_KEY_PARENT_INDEX_NUM, - ITEM_KEY_PRODUCTION_YEAR, - ITEM_KEY_PROVIDER_IDS, - ITEM_KEY_RUNTIME_TICKS, - ITEM_KEY_SORT_NAME, - ITEM_KEY_USER_DATA, - MEDIA_IMAGE_TYPES, - UNKNOWN_ARTIST_MAPPING, - USER_DATA_KEY_IS_FAVORITE, -) - -if TYPE_CHECKING: - from aiojellyfin import Album as JellyAlbum - from aiojellyfin import Artist as JellyArtist - from aiojellyfin import Connection - from aiojellyfin import MediaItem as JellyMediaItem - from aiojellyfin import Playlist as JellyPlaylist - from aiojellyfin import Track as JellyTrack - - -def parse_album( - logger: Logger, instance_id: str, connection: Connection, jellyfin_album: JellyAlbum -) -> Album: - """Parse a Jellyfin Album response to an Album model object.""" - album_id = jellyfin_album[ITEM_KEY_ID] - album = Album( - item_id=album_id, - provider=DOMAIN, - name=jellyfin_album[ITEM_KEY_NAME], - provider_mappings={ - ProviderMapping( - item_id=str(album_id), - provider_domain=DOMAIN, - provider_instance=instance_id, - ) - }, - ) - if ITEM_KEY_PRODUCTION_YEAR in jellyfin_album: - album.year = jellyfin_album[ITEM_KEY_PRODUCTION_YEAR] - album.metadata.images = _get_artwork(instance_id, connection, jellyfin_album) - if ITEM_KEY_OVERVIEW in jellyfin_album: - album.metadata.description = jellyfin_album[ITEM_KEY_OVERVIEW] - if ITEM_KEY_MUSICBRAINZ_ALBUM in jellyfin_album[ITEM_KEY_PROVIDER_IDS]: - try: - album.add_external_id( - ExternalID.MB_ALBUM, - jellyfin_album[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_ALBUM], - ) - except InvalidDataError as error: - logger.warning( - "Jellyfin has an invalid musicbrainz album id for album %s", - album.name, - exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, - ) - if ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP in jellyfin_album[ITEM_KEY_PROVIDER_IDS]: - try: - album.add_external_id( - ExternalID.MB_RELEASEGROUP, - jellyfin_album[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_RELEASE_GROUP], - ) - except InvalidDataError as error: - logger.warning( - "Jellyfin has an invalid musicbrainz id for album %s", - album.name, - exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, - ) - if ITEM_KEY_SORT_NAME in jellyfin_album: - album.sort_name = jellyfin_album[ITEM_KEY_SORT_NAME] - if ITEM_KEY_ALBUM_ARTIST in jellyfin_album: - for album_artist in jellyfin_album[ITEM_KEY_ALBUM_ARTISTS]: - album.artists.append( - ItemMapping( - media_type=MediaType.ARTIST, - item_id=album_artist[ITEM_KEY_ID], - provider=instance_id, - name=album_artist[ITEM_KEY_NAME], - ) - ) - elif len(jellyfin_album.get(ITEM_KEY_ARTIST_ITEMS, [])) >= 1: - for artist_item in jellyfin_album[ITEM_KEY_ARTIST_ITEMS]: - album.artists.append( - ItemMapping( - media_type=MediaType.ARTIST, - item_id=artist_item[ITEM_KEY_ID], - provider=instance_id, - name=artist_item[ITEM_KEY_NAME], - ) - ) - else: - album.artists.append(UNKNOWN_ARTIST_MAPPING) - - user_data = jellyfin_album.get(ITEM_KEY_USER_DATA, {}) - album.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) - return album - - -def parse_artist( - logger: Logger, instance_id: str, connection: Connection, jellyfin_artist: JellyArtist -) -> Artist: - """Parse a Jellyfin Artist response to Artist model object.""" - artist_id = jellyfin_artist[ITEM_KEY_ID] - artist = Artist( - item_id=artist_id, - name=jellyfin_artist[ITEM_KEY_NAME], - provider=DOMAIN, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=DOMAIN, - provider_instance=instance_id, - ) - }, - ) - if ITEM_KEY_OVERVIEW in jellyfin_artist: - artist.metadata.description = jellyfin_artist[ITEM_KEY_OVERVIEW] - if ITEM_KEY_MUSICBRAINZ_ARTIST in jellyfin_artist[ITEM_KEY_PROVIDER_IDS]: - try: - artist.mbid = jellyfin_artist[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_ARTIST] - except InvalidDataError as error: - logger.warning( - "Jellyfin has an invalid musicbrainz id for artist %s", - artist.name, - exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, - ) - if ITEM_KEY_SORT_NAME in jellyfin_artist: - artist.sort_name = jellyfin_artist[ITEM_KEY_SORT_NAME] - artist.metadata.images = _get_artwork(instance_id, connection, jellyfin_artist) - user_data = jellyfin_artist.get(ITEM_KEY_USER_DATA, {}) - artist.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) - return artist - - -def parse_track( - logger: Logger, instance_id: str, client: Connection, jellyfin_track: JellyTrack -) -> Track: - """Parse a Jellyfin Track response to a Track model object.""" - available = False - content = None - available = jellyfin_track[ITEM_KEY_CAN_DOWNLOAD] - content = jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0][ITEM_KEY_MEDIA_CODEC] - track = Track( - item_id=jellyfin_track[ITEM_KEY_ID], - provider=instance_id, - name=jellyfin_track[ITEM_KEY_NAME], - provider_mappings={ - ProviderMapping( - item_id=jellyfin_track[ITEM_KEY_ID], - provider_domain=DOMAIN, - provider_instance=instance_id, - available=available, - audio_format=AudioFormat( - content_type=( - ContentType.try_parse(content) if content else ContentType.UNKNOWN - ), - ), - url=client.audio_url(jellyfin_track[ITEM_KEY_ID]), - ) - }, - ) - - track.disc_number = jellyfin_track.get(ITEM_KEY_PARENT_INDEX_NUM, 0) - track.track_number = jellyfin_track.get("IndexNumber", 0) - if track.track_number is not None and track.track_number >= 0: - track.position = track.track_number - - track.metadata.images = _get_artwork(instance_id, client, jellyfin_track) - - if jellyfin_track[ITEM_KEY_ARTIST_ITEMS]: - for artist_item in jellyfin_track[ITEM_KEY_ARTIST_ITEMS]: - track.artists.append( - ItemMapping( - media_type=MediaType.ARTIST, - item_id=artist_item[ITEM_KEY_ID], - provider=instance_id, - name=artist_item[ITEM_KEY_NAME], - ) - ) - else: - track.artists.append(UNKNOWN_ARTIST_MAPPING) - - if ITEM_KEY_ALBUM_ID in jellyfin_track: - if not (album_name := jellyfin_track.get(ITEM_KEY_ALBUM)): - logger.debug("Track %s has AlbumID but no AlbumName", track.name) - album_name = f"Unknown Album ({jellyfin_track[ITEM_KEY_ALBUM_ID]})" - track.album = ItemMapping( - media_type=MediaType.ALBUM, - item_id=jellyfin_track[ITEM_KEY_ALBUM_ID], - provider=instance_id, - name=album_name, - ) - - if ITEM_KEY_RUNTIME_TICKS in jellyfin_track: - track.duration = int( - jellyfin_track[ITEM_KEY_RUNTIME_TICKS] / 10000000 - ) # 10000000 ticks per millisecond - if ITEM_KEY_MUSICBRAINZ_TRACK in jellyfin_track[ITEM_KEY_PROVIDER_IDS]: - track_mbid = jellyfin_track[ITEM_KEY_PROVIDER_IDS][ITEM_KEY_MUSICBRAINZ_TRACK] - try: - track.mbid = track_mbid - except InvalidDataError as error: - logger.warning( - "Jellyfin has an invalid musicbrainz id for track %s", - track.name, - exc_info=error if logger.isEnabledFor(logging.DEBUG) else None, - ) - user_data = jellyfin_track.get(ITEM_KEY_USER_DATA, {}) - track.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) - return track - - -def parse_playlist( - instance_id: str, client: Connection, jellyfin_playlist: JellyPlaylist -) -> Playlist: - """Parse a Jellyfin Playlist response to a Playlist object.""" - playlistid = jellyfin_playlist[ITEM_KEY_ID] - playlist = Playlist( - item_id=playlistid, - provider=DOMAIN, - name=jellyfin_playlist[ITEM_KEY_NAME], - provider_mappings={ - ProviderMapping( - item_id=playlistid, - provider_domain=DOMAIN, - provider_instance=instance_id, - ) - }, - ) - if ITEM_KEY_OVERVIEW in jellyfin_playlist: - playlist.metadata.description = jellyfin_playlist[ITEM_KEY_OVERVIEW] - playlist.metadata.images = _get_artwork(instance_id, client, jellyfin_playlist) - user_data = jellyfin_playlist.get(ITEM_KEY_USER_DATA, {}) - playlist.favorite = user_data.get(USER_DATA_KEY_IS_FAVORITE, False) - playlist.is_editable = False - return playlist - - -def _get_artwork( - instance_id: str, client: Connection, media_item: JellyMediaItem -) -> UniqueList[MediaItemImage]: - images: UniqueList[MediaItemImage] = UniqueList() - - for i, _ in enumerate(media_item.get("BackdropImageTags", [])): - images.append( - MediaItemImage( - type=ImageType.FANART, - path=client.artwork(media_item[ITEM_KEY_ID], JellyImageType.Backdrop, index=i), - provider=instance_id, - remotely_accessible=False, - ) - ) - - image_tags = media_item[ITEM_KEY_IMAGE_TAGS] - for jelly_image_type, image_type in MEDIA_IMAGE_TYPES.items(): - if jelly_image_type in image_tags: - images.append( - MediaItemImage( - type=image_type, - path=client.artwork(media_item[ITEM_KEY_ID], jelly_image_type), - provider=instance_id, - remotely_accessible=False, - ) - ) - - return images diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/server/providers/musicbrainz/__init__.py deleted file mode 100644 index 41522646..00000000 --- a/music_assistant/server/providers/musicbrainz/__init__.py +++ /dev/null @@ -1,425 +0,0 @@ -"""The Musicbrainz Metadata provider for Music Assistant. - -At this time only used for retrieval of ID's but to be expanded to fetch metadata too. -""" - -from __future__ import annotations - -import re -from contextlib import suppress -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any - -from mashumaro import DataClassDictMixin -from mashumaro.exceptions import MissingField - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.enums import ExternalID, ProviderFeature -from music_assistant.common.models.errors import InvalidDataError, ResourceTemporarilyUnavailable -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.models.metadata_provider import MetadataProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) - from music_assistant.common.models.media_items import Album, Track - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' - -SUPPORTED_FEATURES = () - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return MusicbrainzProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return () # we do not have any config entries (yet) - - -def replace_hyphens(data: dict[str, Any]) -> dict[str, Any]: - """Change all hyphens to underscores.""" - new_values = {} - for key, value in data.items(): - new_key = key.replace("-", "_") - if isinstance(value, dict): - new_values[new_key] = replace_hyphens(value) - elif isinstance(value, list): - new_values[new_key] = [replace_hyphens(x) if isinstance(x, dict) else x for x in value] - else: - new_values[new_key] = value - return new_values - - -@dataclass -class MusicBrainzTag(DataClassDictMixin): - """Model for a (basic) Tag object as received from the MusicBrainz API.""" - - count: int - name: str - - -@dataclass -class MusicBrainzAlias(DataClassDictMixin): - """Model for a (basic) Alias object from MusicBrainz.""" - - name: str - sort_name: str - - # optional fields - locale: str | None = None - type: str | None = None - primary: bool | None = None - begin_date: str | None = None - end_date: str | None = None - - -@dataclass -class MusicBrainzArtist(DataClassDictMixin): - """Model for a (basic) Artist object from MusicBrainz.""" - - id: str - name: str - sort_name: str - - # optional fields - aliases: list[MusicBrainzAlias] | None = None - tags: list[MusicBrainzTag] | None = None - - -@dataclass -class MusicBrainzArtistCredit(DataClassDictMixin): - """Model for a (basic) ArtistCredit object from MusicBrainz.""" - - name: str - artist: MusicBrainzArtist - - -@dataclass -class MusicBrainzReleaseGroup(DataClassDictMixin): - """Model for a (basic) ReleaseGroup object from MusicBrainz.""" - - id: str - title: str - - # optional fields - primary_type: str | None = None - primary_type_id: str | None = None - secondary_types: list[str] | None = None - secondary_type_ids: list[str] | None = None - artist_credit: list[MusicBrainzArtistCredit] | None = None - - -@dataclass -class MusicBrainzTrack(DataClassDictMixin): - """Model for a (basic) Track object from MusicBrainz.""" - - id: str - number: str - title: str - length: int | None = None - - -@dataclass -class MusicBrainzMedia(DataClassDictMixin): - """Model for a (basic) Media object from MusicBrainz.""" - - format: str - track: list[MusicBrainzTrack] - position: int = 0 - track_count: int = 0 - track_offset: int = 0 - - -@dataclass -class MusicBrainzRelease(DataClassDictMixin): - """Model for a (basic) Release object from MusicBrainz.""" - - id: str - status_id: str - count: int - title: str - status: str - artist_credit: list[MusicBrainzArtistCredit] - release_group: MusicBrainzReleaseGroup - track_count: int = 0 - - # optional fields - media: list[MusicBrainzMedia] = field(default_factory=list) - date: str | None = None - country: str | None = None - disambiguation: str | None = None # version - # TODO (if needed): release-events - - -@dataclass -class MusicBrainzRecording(DataClassDictMixin): - """Model for a (basic) Recording object as received from the MusicBrainz API.""" - - id: str - title: str - artist_credit: list[MusicBrainzArtistCredit] = field(default_factory=list) - # optional fields - length: int | None = None - first_release_date: str | None = None - isrcs: list[str] | None = None - tags: list[MusicBrainzTag] | None = None - disambiguation: str | None = None # version (e.g. live, karaoke etc.) - - -class MusicbrainzProvider(MetadataProvider): - """The Musicbrainz Metadata provider.""" - - throttler = ThrottlerManager(rate_limit=1, period=30) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.cache = self.mass.cache - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def search( - self, artistname: str, albumname: str, trackname: str, trackversion: str | None = None - ) -> tuple[MusicBrainzArtist, MusicBrainzReleaseGroup, MusicBrainzRecording] | None: - """ - Search MusicBrainz details by providing the artist, album and track name. - - NOTE: The MusicBrainz objects returned are simplified objects without the optional data. - """ - trackname, trackversion = parse_title_and_version(trackname, trackversion) - searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname) - searchalbum = re.sub(LUCENE_SPECIAL, r"\\\1", albumname) - searchtracks: list[str] = [] - if trackversion: - searchtracks.append(f"{trackname} ({trackversion})") - searchtracks.append(trackname) - # the version is sometimes appended to the title and sometimes stored - # in disambiguation, so we try both - for strict in (True, False): - for searchtrack in searchtracks: - searchstr = re.sub(LUCENE_SPECIAL, r"\\\1", searchtrack) - result = await self.get_data( - "recording", - query=f'"{searchstr}" AND artist:"{searchartist}" AND release:"{searchalbum}"', - ) - if not result or "recordings" not in result: - continue - for item in result["recordings"]: - # compare track title - if not compare_strings(item["title"], searchtrack, strict): - continue - # compare track version if needed - if ( - trackversion - and trackversion not in searchtrack - and not compare_strings(item.get("disambiguation"), trackversion, strict) - ): - continue - # match (primary) track artist - artist_match: MusicBrainzArtist | None = None - for artist in item["artist-credit"]: - if compare_strings(artist["artist"]["name"], artistname, strict): - artist_match = MusicBrainzArtist.from_dict( - replace_hyphens(artist["artist"]) - ) - else: - for alias in artist["artist"].get("aliases", []): - if compare_strings(alias["name"], artistname, strict): - artist_match = MusicBrainzArtist.from_dict( - replace_hyphens(artist["artist"]) - ) - if not artist_match: - continue - # match album/release - album_match: MusicBrainzReleaseGroup | None = None - for release in item["releases"]: - if compare_strings(release["title"], albumname, strict) or compare_strings( - release["release-group"]["title"], albumname, strict - ): - album_match = MusicBrainzReleaseGroup.from_dict( - replace_hyphens(release["release-group"]) - ) - break - else: - continue - # if we reach this point, we got a match on recording, - # artist and release(group) - recording = MusicBrainzRecording.from_dict(replace_hyphens(item)) - return (artist_match, album_match, recording) - - return None - - async def get_artist_details(self, artist_id: str) -> MusicBrainzArtist: - """Get (full) Artist details by providing a MusicBrainz artist id.""" - endpoint = ( - f"artist/{artist_id}?inc=aliases+annotation+tags+ratings+genres+url-rels+work-rels" - ) - if result := await self.get_data(endpoint): - if "id" not in result: - result["id"] = artist_id - # TODO: Parse all the optional data like relations and such - try: - return MusicBrainzArtist.from_dict(replace_hyphens(result)) - except MissingField as err: - raise InvalidDataError from err - msg = "Invalid MusicBrainz Artist ID provided" - raise InvalidDataError(msg) - - async def get_recording_details(self, recording_id: str) -> MusicBrainzRecording: - """Get Recording details by providing a MusicBrainz Recording Id.""" - if result := await self.get_data(f"recording/{recording_id}?inc=artists+releases"): - if "id" not in result: - result["id"] = recording_id - try: - return MusicBrainzRecording.from_dict(replace_hyphens(result)) - except MissingField as err: - raise InvalidDataError from err - msg = "Invalid MusicBrainz recording ID provided" - raise InvalidDataError(msg) - - async def get_release_details(self, album_id: str) -> MusicBrainzRelease: - """Get Release/Album details by providing a MusicBrainz Album id.""" - endpoint = f"release/{album_id}?inc=artist-credits+aliases+labels" - if result := await self.get_data(endpoint): - if "id" not in result: - result["id"] = album_id - try: - return MusicBrainzRelease.from_dict(replace_hyphens(result)) - except MissingField as err: - raise InvalidDataError from err - msg = "Invalid MusicBrainz Album ID provided" - raise InvalidDataError(msg) - - async def get_releasegroup_details(self, releasegroup_id: str) -> MusicBrainzReleaseGroup: - """Get ReleaseGroup details by providing a MusicBrainz ReleaseGroup id.""" - endpoint = f"release-group/{releasegroup_id}?inc=artists+aliases" - if result := await self.get_data(endpoint): - if "id" not in result: - result["id"] = releasegroup_id - try: - return MusicBrainzReleaseGroup.from_dict(replace_hyphens(result)) - except MissingField as err: - raise InvalidDataError from err - msg = "Invalid MusicBrainz ReleaseGroup ID provided" - raise InvalidDataError(msg) - - async def get_artist_details_by_album( - self, artistname: str, ref_album: Album - ) -> MusicBrainzArtist | None: - """ - Get musicbrainz artist details by providing the artist name and a reference album. - - MusicBrainzArtist object that is returned does not contain the optional data. - """ - result = None - if mb_id := ref_album.get_external_id(ExternalID.MB_RELEASEGROUP): - with suppress(InvalidDataError): - result = await self.get_releasegroup_details(mb_id) - elif mb_id := ref_album.get_external_id(ExternalID.MB_ALBUM): - with suppress(InvalidDataError): - result = await self.get_release_details(mb_id) - else: - return None - if not (result and result.artist_credit): - return None - for strict in (True, False): - for artist_credit in result.artist_credit: - if compare_strings(artist_credit.artist.name, artistname, strict): - return artist_credit.artist - for alias in artist_credit.artist.aliases or []: - if compare_strings(alias.name, artistname, strict): - return artist_credit.artist - return None - - async def get_artist_details_by_track( - self, artistname: str, ref_track: Track - ) -> MusicBrainzArtist | None: - """ - Get musicbrainz artist details by providing the artist name and a reference track. - - MusicBrainzArtist object that is returned does not contain the optional data. - """ - if not ref_track.mbid: - return None - result = None - with suppress(InvalidDataError): - result = await self.get_recording_details(ref_track.mbid) - if not (result and result.artist_credit): - return None - for strict in (True, False): - for artist_credit in result.artist_credit: - if compare_strings(artist_credit.artist.name, artistname, strict): - return artist_credit.artist - for alias in artist_credit.artist.aliases or []: - if compare_strings(alias.name, artistname, strict): - return artist_credit.artist - return None - - async def get_artist_details_by_resource_url( - self, resource_url: str - ) -> MusicBrainzArtist | None: - """ - Get musicbrainz artist details by providing a resource URL (e.g. Spotify share URL). - - MusicBrainzArtist object that is returned does not contain the optional data. - """ - if result := await self.get_data("url", resource=resource_url, inc="artist-rels"): - for relation in result.get("relations", []): - if not (artist := relation.get("artist")): - continue - return MusicBrainzArtist.from_dict(replace_hyphens(artist)) - return None - - @use_cache(86400 * 30) - @throttle_with_retries - async def get_data(self, endpoint: str, **kwargs: dict[str, Any]) -> Any: - """Get data from api.""" - url = f"http://musicbrainz.org/ws/2/{endpoint}" - headers = { - "User-Agent": f"Music Assistant/{self.mass.version} (https://music-assistant.io)" - } - kwargs["fmt"] = "json" # type: ignore[assignment] - async with ( - self.mass.http_session.get(url, headers=headers, params=kwargs) as response, - ): - # handle rate limiter - if response.status == 429: - backoff_time = int(response.headers.get("Retry-After", 0)) - raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - # handle 404 not found - if response.status in (400, 401, 404): - return None - response.raise_for_status() - return await response.json(loads=json_loads) diff --git a/music_assistant/server/providers/musicbrainz/icon.svg b/music_assistant/server/providers/musicbrainz/icon.svg deleted file mode 100644 index fde0f687..00000000 --- a/music_assistant/server/providers/musicbrainz/icon.svg +++ /dev/null @@ -1 +0,0 @@ -MusicBrainz diff --git a/music_assistant/server/providers/musicbrainz/icon_dark.svg b/music_assistant/server/providers/musicbrainz/icon_dark.svg deleted file mode 100644 index 249e9ada..00000000 --- a/music_assistant/server/providers/musicbrainz/icon_dark.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - -MusicBrainz icon - - - - - - diff --git a/music_assistant/server/providers/musicbrainz/manifest.json b/music_assistant/server/providers/musicbrainz/manifest.json deleted file mode 100644 index f3c48835..00000000 --- a/music_assistant/server/providers/musicbrainz/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "metadata", - "domain": "musicbrainz", - "name": "MusicBrainz", - "description": "MusicBrainz is an open music encyclopedia that collects music metadata and makes it available to the public. Music Assistant uses MusicBrainz primarily to identify (unique) media items and therefore this provider can not be disabled. However, note that lookups will only be performed if this info is absent locally.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "", - "multi_instance": false, - "builtin": true, - "allow_disable": false, - "icon": "mdi-folder-information" -} diff --git a/music_assistant/server/providers/opensubsonic/__init__.py b/music_assistant/server/providers/opensubsonic/__init__.py deleted file mode 100644 index 4f6a42f6..00000000 --- a/music_assistant/server/providers/opensubsonic/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Open Subsonic music provider support for MusicAssistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.constants import CONF_PASSWORD, CONF_PATH, CONF_PORT, CONF_USERNAME - -from .sonic_provider import ( - CONF_BASE_URL, - CONF_ENABLE_LEGACY_AUTH, - CONF_ENABLE_PODCASTS, - OpenSonicProvider, -) - -if TYPE_CHECKING: - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return OpenSonicProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, # noqa: ARG001 - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """Return Config entries to setup this provider.""" - return ( - ConfigEntry( - key=CONF_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - description="Your username for this Open Subsonic server", - ), - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password", - required=True, - description="The password associated with the username", - ), - ConfigEntry( - key=CONF_BASE_URL, - type=ConfigEntryType.STRING, - label="Base URL", - required=True, - description="Base URL for the server, e.g. " "https://subsonic.mydomain.tld", - ), - ConfigEntry( - key=CONF_PORT, - type=ConfigEntryType.INTEGER, - label="Port", - required=False, - description="Port Number for the server", - ), - ConfigEntry( - key=CONF_PATH, - type=ConfigEntryType.STRING, - label="Server Path", - required=False, - description="Path to append to base URL for Soubsonic server, this is likely " - "empty unless you are path routing on a proxy", - ), - ConfigEntry( - key=CONF_ENABLE_PODCASTS, - type=ConfigEntryType.BOOLEAN, - label="Enable Podcasts", - required=True, - description="Should the provider query for podcasts as well as music?", - default_value=True, - ), - ConfigEntry( - key=CONF_ENABLE_LEGACY_AUTH, - type=ConfigEntryType.BOOLEAN, - label="Enable legacy auth", - required=True, - description='Enable OpenSubsonic "legacy" auth support', - default_value=False, - ), - ) diff --git a/music_assistant/server/providers/opensubsonic/icon.svg b/music_assistant/server/providers/opensubsonic/icon.svg deleted file mode 100644 index 429336ab..00000000 --- a/music_assistant/server/providers/opensubsonic/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - Created by potrace 1.16, written by Peter Selinger 2001-2019 - - - - - diff --git a/music_assistant/server/providers/opensubsonic/manifest.json b/music_assistant/server/providers/opensubsonic/manifest.json deleted file mode 100644 index 002cb5be..00000000 --- a/music_assistant/server/providers/opensubsonic/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "music", - "domain": "opensubsonic", - "name": "Open Subsonic Media Server Library", - "description": "Support for Open Subsonic based streaming providers in Music Assistant.", - "codeowners": [ - "@khers" - ], - "requirements": [ - "py-opensonic==5.1.1" - ], - "documentation": "https://music-assistant.io/music-providers/subsonic/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/opensubsonic/sonic_provider.py b/music_assistant/server/providers/opensubsonic/sonic_provider.py deleted file mode 100644 index 556cc2ba..00000000 --- a/music_assistant/server/providers/opensubsonic/sonic_provider.py +++ /dev/null @@ -1,847 +0,0 @@ -"""The provider class for Open Subsonic.""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -from libopensonic.connection import Connection as SonicConnection -from libopensonic.errors import ( - AuthError, - CredentialError, - DataNotFoundError, - ParameterError, - SonicError, -) - -from music_assistant.common.models.enums import ( - ContentType, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - LoginFailed, - MediaNotFoundError, - ProviderPermissionDenied, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - AudioFormat, - ItemMapping, - MediaItemImage, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import ( - CONF_PASSWORD, - CONF_PATH, - CONF_PORT, - CONF_USERNAME, - UNKNOWN_ARTIST, -) -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable - - from libopensonic.media import Album as SonicAlbum - from libopensonic.media import AlbumInfo as SonicAlbumInfo - from libopensonic.media import Artist as SonicArtist - from libopensonic.media import ArtistInfo as SonicArtistInfo - from libopensonic.media import Playlist as SonicPlaylist - from libopensonic.media import PodcastChannel as SonicPodcastChannel - from libopensonic.media import PodcastEpisode as SonicPodcastEpisode - from libopensonic.media import Song as SonicSong - -CONF_BASE_URL = "baseURL" -CONF_ENABLE_PODCASTS = "enable_podcasts" -CONF_ENABLE_LEGACY_AUTH = "enable_legacy_auth" - -UNKNOWN_ARTIST_ID = "fake_artist_unknown" - -# We need the following prefix because of the way that Navidrome reports artists for individual -# tracks on Various Artists albums, see the note in the _parse_track() method and the handling -# in get_artist() -NAVI_VARIOUS_PREFIX = "MA-NAVIDROME-" - - -class OpenSonicProvider(MusicProvider): - """Provider for Open Subsonic servers.""" - - _conn: SonicConnection = None - _enable_podcasts: bool = True - _seek_support: bool = False - - async def handle_async_init(self) -> None: - """Set up the music provider and test the connection.""" - port = self.config.get_value(CONF_PORT) - if port is None: - port = 443 - path = self.config.get_value(CONF_PATH) - if path is None: - path = "" - self._conn = SonicConnection( - self.config.get_value(CONF_BASE_URL), - username=self.config.get_value(CONF_USERNAME), - password=self.config.get_value(CONF_PASSWORD), - legacyAuth=self.config.get_value(CONF_ENABLE_LEGACY_AUTH), - port=port, - serverPath=path, - appName="Music Assistant", - ) - try: - success = await self._run_async(self._conn.ping) - if not success: - msg = ( - f"Failed to connect to {self.config.get_value(CONF_BASE_URL)}, " - "check your settings." - ) - raise LoginFailed(msg) - except (AuthError, CredentialError) as e: - msg = ( - f"Failed to connect to {self.config.get_value(CONF_BASE_URL)}, check your settings." - ) - raise LoginFailed(msg) from e - self._enable_podcasts = self.config.get_value(CONF_ENABLE_PODCASTS) - try: - ret = await self._run_async(self._conn.getOpenSubsonicExtensions) - extensions = ret["openSubsonicExtensions"] - for entry in extensions: - if entry["name"] == "transcodeOffset": - self._seek_support = True - break - except OSError: - self.logger.info("Server does not support transcodeOffset, seeking in player provider") - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return a list of supported features.""" - return ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ProviderFeature.PLAYLIST_CREATE, - ) - - @property - def is_streaming_provider(self) -> bool: - """ - Return True if the provider is a streaming provider. - - This literally means that the catalog is not the same as the library contents. - For local based providers (files, plex), the catalog is the same as the library content. - It also means that data is if this provider is NOT a streaming provider, - data cross instances is unique, the catalog and library differs per instance. - - Setting this to True will only query one instance of the provider for search and lookups. - Setting this to False will query all instances of this provider for search and lookups. - """ - return False - - def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: - return ItemMapping( - media_type=media_type, - item_id=key, - provider=self.instance_id, - name=name, - ) - - def _parse_podcast_artist(self, sonic_channel: SonicPodcastChannel) -> Artist: - artist = Artist( - item_id=sonic_channel.id, - name=sonic_channel.title, - provider=self.instance_id, - favorite=bool(sonic_channel.starred), - provider_mappings={ - ProviderMapping( - item_id=sonic_channel.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - if sonic_channel.description is not None: - artist.metadata.description = sonic_channel.description - if sonic_channel.original_image_url: - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=sonic_channel.original_image_url, - provider=self.instance_id, - remotely_accessible=True, - ) - ] - return artist - - def _parse_podcast_album(self, sonic_channel: SonicPodcastChannel) -> Album: - return Album( - item_id=sonic_channel.id, - provider=self.instance_id, - name=sonic_channel.title, - provider_mappings={ - ProviderMapping( - item_id=sonic_channel.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=True, - ) - }, - album_type=AlbumType.PODCAST, - ) - - def _parse_podcast_episode( - self, sonic_episode: SonicPodcastEpisode, sonic_channel: SonicPodcastChannel - ) -> Track: - return Track( - item_id=sonic_episode.id, - provider=self.instance_id, - name=sonic_episode.title, - album=self._parse_podcast_album(sonic_channel=sonic_channel), - artists=[self._parse_podcast_artist(sonic_channel=sonic_channel)], - duration=sonic_episode.duration if sonic_episode.duration is not None else 0, - favorite=bool(sonic_episode.starred), - provider_mappings={ - ProviderMapping( - item_id=sonic_episode.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=True, - ) - }, - ) - - async def _get_podcast_artists(self) -> list[Artist]: - if not self._enable_podcasts: - return [] - - sonic_channels = await self._run_async(self._conn.getPodcasts, incEpisodes=False) - artists = [] - for channel in sonic_channels: - artists.append(self._parse_podcast_artist(channel)) - return artists - - async def _get_podcasts(self) -> list[SonicPodcastChannel]: - if not self._enable_podcasts: - return [] - return await self._run_async(self._conn.getPodcasts, incEpisodes=True) - - def _parse_artist( - self, sonic_artist: SonicArtist, sonic_info: SonicArtistInfo = None - ) -> Artist: - artist = Artist( - item_id=sonic_artist.id, - name=sonic_artist.name, - provider=self.domain, - favorite=bool(sonic_artist.starred), - provider_mappings={ - ProviderMapping( - item_id=sonic_artist.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - - if sonic_artist.cover_id: - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=sonic_artist.cover_id, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - else: - artist.metadata.images = [] - - if sonic_info: - if sonic_info.biography: - artist.metadata.description = sonic_info.biography - if sonic_info.small_url: - artist.metadata.images.append( - MediaItemImage( - type=ImageType.THUMB, - path=sonic_info.small_url, - provider=self.instance_id, - remotely_accessible=True, - ) - ) - return artist - - def _parse_album(self, sonic_album: SonicAlbum, sonic_info: SonicAlbumInfo = None) -> Album: - album_id = sonic_album.id - album = Album( - item_id=album_id, - provider=self.domain, - name=sonic_album.name, - favorite=bool(sonic_album.starred), - provider_mappings={ - ProviderMapping( - item_id=album_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - year=sonic_album.year, - ) - - if sonic_album.cover_id: - album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=sonic_album.cover_id, - provider=self.instance_id, - remotely_accessible=False, - ), - ] - else: - album.metadata.images = [] - - if sonic_album.artist_id: - album.artists.append( - self._get_item_mapping( - MediaType.ARTIST, - sonic_album.artist_id, - sonic_album.artist if sonic_album.artist else UNKNOWN_ARTIST, - ) - ) - else: - self.logger.info( - f"Unable to find an artist ID for album '{sonic_album.name}' with " - f"ID '{sonic_album.id}'." - ) - album.artists.append( - Artist( - item_id=UNKNOWN_ARTIST_ID, - name=UNKNOWN_ARTIST, - provider=self.instance_id, - provider_mappings={ - ProviderMapping( - item_id=UNKNOWN_ARTIST_ID, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - ) - - if sonic_info: - if sonic_info.small_url: - album.metadata.images.append( - MediaItemImage( - type=ImageType.THUMB, - path=sonic_info.small_url, - remotely_accessible=False, - provider=self.instance_id, - ) - ) - if sonic_info.notes: - album.metadata.description = sonic_info.notes - - return album - - def _parse_track(self, sonic_song: SonicSong) -> Track: - mapping = None - if sonic_song.album_id is not None and sonic_song.album is not None: - mapping = self._get_item_mapping(MediaType.ALBUM, sonic_song.album_id, sonic_song.album) - - track = Track( - item_id=sonic_song.id, - provider=self.instance_id, - name=sonic_song.title, - album=mapping, - duration=sonic_song.duration if sonic_song.duration is not None else 0, - # We are setting disc number to 0 because the standard for what is part of - # a Open Subsonic Song is not yet set and the implementations I have checked - # do not contain this field. We should revisit this when the spec is finished - disc_number=0, - favorite=bool(sonic_song.starred), - provider_mappings={ - ProviderMapping( - item_id=sonic_song.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=True, - audio_format=AudioFormat( - content_type=ContentType.try_parse(sonic_song.content_type) - ), - ) - }, - track_number=getattr(sonic_song, "track", 0), - ) - - # We need to find an artist for this track but various implementations seem to disagree - # about where the artist with the valid ID needs to be found. We will add any artist with - # an ID and only use UNKNOWN if none are found. - - if sonic_song.artist_id: - track.artists.append( - self._get_item_mapping( - MediaType.ARTIST, - sonic_song.artist_id, - sonic_song.artist if sonic_song.artist else UNKNOWN_ARTIST, - ) - ) - - for entry in sonic_song.artists: - if entry.id == sonic_song.artist_id: - continue - if entry.id is not None and entry.name is not None: - track.artists.append(self._get_item_mapping(MediaType.ARTIST, entry.id, entry.name)) - - if not track.artists: - if sonic_song.artist and not sonic_song.artist_id: - # This is how Navidrome handles tracks from albums which are marked - # 'Various Artists'. Unfortunately, we cannot lookup this artist independently - # because it will not have an entry in the artists table so the best we can do it - # add a 'fake' id with the proper artist name and have get_artist() check for this - # id and handle it locally. - artist = Artist( - item_id=f"{NAVI_VARIOUS_PREFIX}{sonic_song.artist}", - provider=self.domain, - name=sonic_song.artist, - provider_mappings={ - ProviderMapping( - item_id=UNKNOWN_ARTIST_ID, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - else: - self.logger.info( - f"Unable to find artist ID for track '{sonic_song.title}' with " - f"ID '{sonic_song.id}'." - ) - artist = Artist( - item_id=UNKNOWN_ARTIST_ID, - name=UNKNOWN_ARTIST, - provider=self.instance_id, - provider_mappings={ - ProviderMapping( - item_id=UNKNOWN_ARTIST_ID, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - - track.artists.append(artist) - return track - - def _parse_playlist(self, sonic_playlist: SonicPlaylist) -> Playlist: - playlist = Playlist( - item_id=sonic_playlist.id, - provider=self.domain, - name=sonic_playlist.name, - is_editable=True, - favorite=bool(sonic_playlist.starred), - provider_mappings={ - ProviderMapping( - item_id=sonic_playlist.id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - if sonic_playlist.cover_id: - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=sonic_playlist.cover_id, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - return playlist - - async def _run_async(self, call: Callable, *args, **kwargs): - return await self.mass.create_task(call, *args, **kwargs) - - async def resolve_image(self, path: str) -> bytes: - """Return the image.""" - - def _get_cover_art() -> bytes: - with self._conn.getCoverArt(path) as art: - return art.content - - return await asyncio.to_thread(_get_cover_art) - - async def search( - self, search_query: str, media_types: list[MediaType], limit: int = 20 - ) -> SearchResults: - """Search the sonic library.""" - artists = limit if MediaType.ARTIST in media_types else 0 - albums = limit if MediaType.ALBUM in media_types else 0 - songs = limit if MediaType.TRACK in media_types else 0 - if not (artists or albums or songs): - return SearchResults() - answer = await self._run_async( - self._conn.search3, - query=search_query, - artistCount=artists, - artistOffset=0, - albumCount=albums, - albumOffset=0, - songCount=songs, - songOffset=0, - musicFolderId=None, - ) - return SearchResults( - artists=[self._parse_artist(entry) for entry in answer["artists"]], - albums=[self._parse_album(entry) for entry in answer["albums"]], - tracks=[self._parse_track(entry) for entry in answer["songs"]], - ) - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Provide a generator for reading all artists.""" - indices = await self._run_async(self._conn.getArtists) - for index in indices: - for artist in index.artists: - yield self._parse_artist(artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """ - Provide a generator for reading all artists. - - Note the pagination, the open subsonic docs say that this method is limited to - returning 500 items per invocation. - """ - offset = 0 - size = 500 - albums = await self._run_async( - self._conn.getAlbumList2, ltype="alphabeticalByArtist", size=size, offset=offset - ) - while albums: - for album in albums: - yield self._parse_album(album) - offset += size - albums = await self._run_async( - self._conn.getAlbumList2, ltype="alphabeticalByArtist", size=size, offset=offset - ) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Provide a generator for library playlists.""" - results = await self._run_async(self._conn.getPlaylists) - for entry in results: - yield self._parse_playlist(entry) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """ - Provide a generator for library tracks. - - Note the lack of item count on this method. - """ - query = "" - offset = 0 - count = 500 - try: - results = await self._run_async( - self._conn.search3, - query=query, - artistCount=0, - albumCount=0, - songOffset=offset, - songCount=count, - ) - except ParameterError: - # Older Navidrome does not accept an empty string and requires the empty quotes - query = '""' - results = await self._run_async( - self._conn.search3, - query=query, - artistCount=0, - albumCount=0, - songOffset=offset, - songCount=count, - ) - while results["songs"]: - for entry in results["songs"]: - yield self._parse_track(entry) - offset += count - results = await self._run_async( - self._conn.search3, - query=query, - artistCount=0, - albumCount=0, - songOffset=offset, - songCount=count, - ) - - async def get_album(self, prov_album_id: str) -> Album: - """Return the requested Album.""" - try: - sonic_album: SonicAlbum = await self._run_async(self._conn.getAlbum, prov_album_id) - sonic_info = await self._run_async(self._conn.getAlbumInfo2, aid=prov_album_id) - except (ParameterError, DataNotFoundError) as e: - if self._enable_podcasts: - # This might actually be a 'faked' album from podcasts, try that before giving up - try: - sonic_channel = await self._run_async( - self._conn.getPodcasts, incEpisodes=False, pid=prov_album_id - ) - return self._parse_podcast_album(sonic_channel=sonic_channel) - except SonicError: - pass - msg = f"Album {prov_album_id} not found" - raise MediaNotFoundError(msg) from e - - return self._parse_album(sonic_album, sonic_info) - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Return a list of tracks on the specified Album.""" - try: - sonic_album: SonicAlbum = await self._run_async(self._conn.getAlbum, prov_album_id) - except (ParameterError, DataNotFoundError) as e: - msg = f"Album {prov_album_id} not found" - raise MediaNotFoundError(msg) from e - tracks = [] - for sonic_song in sonic_album.songs: - tracks.append(self._parse_track(sonic_song)) - return tracks - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Return the requested Artist.""" - if prov_artist_id == UNKNOWN_ARTIST_ID: - return Artist( - item_id=UNKNOWN_ARTIST_ID, - name=UNKNOWN_ARTIST, - provider=self.instance_id, - provider_mappings={ - ProviderMapping( - item_id=UNKNOWN_ARTIST_ID, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - elif prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): - # Special case for handling track artists on various artists album for Navidrome. - return Artist( - item_id=prov_artist_id, - name=prov_artist_id.removeprefix(NAVI_VARIOUS_PREFIX), - provider=self.instance_id, - provider_mappings={ - ProviderMapping( - item_id=prov_artist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - - try: - sonic_artist: SonicArtist = await self._run_async( - self._conn.getArtist, artist_id=prov_artist_id - ) - sonic_info = await self._run_async(self._conn.getArtistInfo2, aid=prov_artist_id) - except (ParameterError, DataNotFoundError) as e: - if self._enable_podcasts: - # This might actually be a 'faked' artist from podcasts, try that before giving up - try: - sonic_channel = await self._run_async( - self._conn.getPodcasts, incEpisodes=False, pid=prov_artist_id - ) - return self._parse_podcast_artist(sonic_channel=sonic_channel[0]) - except SonicError: - pass - msg = f"Artist {prov_artist_id} not found" - raise MediaNotFoundError(msg) from e - return self._parse_artist(sonic_artist, sonic_info) - - async def get_track(self, prov_track_id: str) -> Track: - """Return the specified track.""" - try: - sonic_song: SonicSong = await self._run_async(self._conn.getSong, prov_track_id) - except (ParameterError, DataNotFoundError) as e: - msg = f"Item {prov_track_id} not found" - raise MediaNotFoundError(msg) from e - return self._parse_track(sonic_song) - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Return a list of all Albums by specified Artist.""" - if prov_artist_id == UNKNOWN_ARTIST_ID or prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): - return [] - - try: - sonic_artist: SonicArtist = await self._run_async(self._conn.getArtist, prov_artist_id) - except (ParameterError, DataNotFoundError) as e: - msg = f"Album {prov_artist_id} not found" - raise MediaNotFoundError(msg) from e - albums = [] - for entry in sonic_artist.albums: - albums.append(self._parse_album(entry)) - return albums - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Return the specified Playlist.""" - try: - sonic_playlist: SonicPlaylist = await self._run_async( - self._conn.getPlaylist, prov_playlist_id - ) - except (ParameterError, DataNotFoundError) as e: - msg = f"Playlist {prov_playlist_id} not found" - raise MediaNotFoundError(msg) from e - return self._parse_playlist(sonic_playlist) - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # paging not supported, we always return the whole list at once - return result - try: - sonic_playlist: SonicPlaylist = await self._run_async( - self._conn.getPlaylist, prov_playlist_id - ) - except (ParameterError, DataNotFoundError) as e: - msg = f"Playlist {prov_playlist_id} not found" - raise MediaNotFoundError(msg) from e - - # TODO: figure out if subsonic supports paging here - for index, sonic_song in enumerate(sonic_playlist.songs, 1): - track = self._parse_track(sonic_song) - track.position = index - result.append(track) - return result - - async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: - """Get the top listed tracks for a specified artist.""" - # We have seen top tracks requested for the UNKNOWN_ARTIST ID, protect against that - if prov_artist_id == UNKNOWN_ARTIST_ID or prov_artist_id.startswith(NAVI_VARIOUS_PREFIX): - return [] - - try: - sonic_artist: SonicArtist = await self._run_async(self._conn.getArtist, prov_artist_id) - except DataNotFoundError as e: - msg = f"Artist {prov_artist_id} not found" - raise MediaNotFoundError(msg) from e - songs: list[SonicSong] = await self._run_async(self._conn.getTopSongs, sonic_artist.name) - return [self._parse_track(entry) for entry in songs] - - async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: - """Get tracks similar to selected track.""" - songs: list[SonicSong] = await self._run_async( - self._conn.getSimilarSongs2, iid=prov_track_id, count=limit - ) - return [self._parse_track(entry) for entry in songs] - - async def create_playlist(self, name: str) -> Playlist: - """Create a new empty playlist on the server.""" - playlist: SonicPlaylist = await self._run_async(self._conn.createPlaylist, name=name) - return self._parse_playlist(playlist) - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Append the listed tracks to the selected playlist. - - Note that the configured user must own the playlist to edit this way. - """ - try: - await self._run_async( - self._conn.updatePlaylist, lid=prov_playlist_id, songIdsToAdd=prov_track_ids - ) - except SonicError: - msg = f"Failed to add songs to {prov_playlist_id}, check your permissions." - raise ProviderPermissionDenied(msg) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove selected positions from the playlist.""" - idx_to_remove = [pos - 1 for pos in positions_to_remove] - try: - await self._run_async( - self._conn.updatePlaylist, - lid=prov_playlist_id, - songIndexesToRemove=idx_to_remove, - ) - except SonicError: - msg = f"Failed to remove songs from {prov_playlist_id}, check your permissions." - raise ProviderPermissionDenied(msg) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get the details needed to process a specified track.""" - try: - sonic_song: SonicSong = await self._run_async(self._conn.getSong, item_id) - except (ParameterError, DataNotFoundError) as e: - msg = f"Item {item_id} not found" - raise MediaNotFoundError(msg) from e - - self.mass.create_task(self._report_playback_started(item_id)) - - mime_type = sonic_song.content_type - if mime_type.endswith("mpeg"): - mime_type = sonic_song.suffix - - self.logger.debug( - "Fetching stream details for id %s '%s' with format '%s'", - sonic_song.id, - sonic_song.title, - mime_type, - ) - - return StreamDetails( - item_id=sonic_song.id, - provider=self.instance_id, - can_seek=self._seek_support, - audio_format=AudioFormat(content_type=ContentType.try_parse(mime_type)), - stream_type=StreamType.CUSTOM, - duration=sonic_song.duration if sonic_song.duration is not None else 0, - ) - - async def _report_playback_started(self, item_id: str) -> None: - await self._run_async(self._conn.scrobble, sid=item_id, submission=False) - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - if seconds_streamed >= streamdetails.duration / 2: - await self._run_async(self._conn.scrobble, sid=streamdetails.item_id, submission=True) - - async def get_audio_stream( - self, streamdetails: StreamDetails, seek_position: int = 0 - ) -> AsyncGenerator[bytes, None]: - """Provide a generator for the stream data.""" - audio_buffer = asyncio.Queue(1) - - self.logger.debug("Streaming %s", streamdetails.item_id) - - def _streamer() -> None: - with self._conn.stream( - streamdetails.item_id, timeOffset=seek_position, estimateContentLength=True - ) as stream: - for chunk in stream.iter_content(chunk_size=40960): - asyncio.run_coroutine_threadsafe( - audio_buffer.put(chunk), self.mass.loop - ).result() - # send empty chunk when we're done - asyncio.run_coroutine_threadsafe(audio_buffer.put(b"EOF"), self.mass.loop).result() - - # fire up an executor thread to put the audio chunks (threadsafe) on the audio buffer - streamer_task = self.mass.loop.run_in_executor(None, _streamer) - try: - while True: - # keep reading from the audio buffer until there is no more data - chunk = await audio_buffer.get() - if chunk == b"EOF": - break - yield chunk - finally: - if not streamer_task.done(): - streamer_task.cancel() - - self.logger.debug("Done streaming %s", streamdetails.item_id) diff --git a/music_assistant/server/providers/player_group/__init__.py b/music_assistant/server/providers/player_group/__init__.py deleted file mode 100644 index b3b5c6f0..00000000 --- a/music_assistant/server/providers/player_group/__init__.py +++ /dev/null @@ -1,852 +0,0 @@ -""" -Sync Group Player provider. - -This is more like a "virtual" player provider, -allowing the user to create 'presets' of players to sync together (of the same type). -""" - -from __future__ import annotations - -from collections.abc import Callable -from contextlib import suppress -from time import time -from typing import TYPE_CHECKING, Final, cast - -import shortuuid -from aiohttp import web - -from music_assistant.common.models.config_entries import ( - BASE_PLAYER_CONFIG_ENTRIES, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_PLAYER_ICON_GROUP, - ConfigEntry, - ConfigValueOption, - ConfigValueType, - PlayerConfig, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - EventType, - MediaType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.errors import ( - PlayerUnavailableError, - ProviderUnavailableError, - UnsupportedFeaturedException, -) -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import ( - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_ENABLE_ICY_METADATA, - CONF_ENFORCE_MP3, - CONF_FLOW_MODE, - CONF_GROUP_MEMBERS, - CONF_HTTP_PROFILE, - CONF_SAMPLE_RATES, -) -from music_assistant.server.controllers.streams import DEFAULT_STREAM_HEADERS -from music_assistant.server.helpers.ffmpeg import get_ffmpeg_stream -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider - -from .ugp_stream import UGP_FORMAT, UGPStream - -if TYPE_CHECKING: - from collections.abc import Iterable - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -# ruff: noqa: ARG002 - -UNIVERSAL_PREFIX: Final[str] = "ugp_" -SYNCGROUP_PREFIX: Final[str] = "syncgroup_" -GROUP_TYPE_UNIVERSAL: Final[str] = "universal" -CONF_GROUP_TYPE: Final[str] = "group_type" -CONF_ENTRY_GROUP_TYPE = ConfigEntry( - key=CONF_GROUP_TYPE, - type=ConfigEntryType.STRING, - label="Group type", - default_value="universal", - hidden=True, - required=True, -) -CONF_ENTRY_GROUP_MEMBERS = ConfigEntry( - key=CONF_GROUP_MEMBERS, - type=ConfigEntryType.STRING, - label="Group members", - default_value=[], - description="Select all players you want to be part of this group", - multi_value=True, - required=True, -) -CONF_ENTRY_SAMPLE_RATES_UGP = create_sample_rates_config_entry(44100, 16, 44100, 16, True) -CONFIG_ENTRY_UGP_NOTE = ConfigEntry( - key="ugp_note", - type=ConfigEntryType.LABEL, - label="Please note that although the Universal Group " - "allows you to group any player, it will not enable audio sync " - "between players of different ecosystems. It is advised to always use native " - "player groups or sync groups when available for your player type(s) and use " - "the Universal Group only to group players of different ecosystems.", - required=False, -) -CONFIG_ENTRY_DYNAMIC_MEMBERS = ConfigEntry( - key="dynamic_members", - type=ConfigEntryType.BOOLEAN, - label="Enable dynamic members (experimental)", - description="Allow members to (temporary) join/leave the group dynamically, " - "so the group more or less behaves the same like manually syncing players together, " - "with the main difference being that the groupplayer will hold the queue. \n\n" - "NOTE: This is an experimental feature which we are testing out. " - "You may run into some unexpected behavior!", - default_value=False, - required=False, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return PlayerGroupProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, # noqa: ARG001 - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # nothing to configure (for now) - return () - - -class PlayerGroupProvider(PlayerProvider): - """Base/builtin provider for creating (permanent) player groups.""" - - def __init__( - self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig - ) -> None: - """Initialize MusicProvider.""" - super().__init__(mass, manifest, config) - self.ugp_streams: dict[str, UGPStream] = {} - self._on_unload: list[Callable[[], None]] = [ - self.mass.register_api_command("player_group/create", self.create_group), - ] - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.REMOVE_PLAYER,) - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - await super().loaded_in_mass() - # temp: migrate old config entries - # remove this after MA 2.4 release - for player_config in await self.mass.config.get_player_configs(): - if player_config.provider == self.instance_id: - # already migrated - continue - # migrate old syncgroup players to this provider - if player_config.player_id.startswith(SYNCGROUP_PREFIX): - self.mass.config.set_raw_player_config_value( - player_config.player_id, CONF_GROUP_TYPE, player_config.provider - ) - player_config.provider = self.instance_id - self.mass.config.set_raw_player_config_value( - player_config.player_id, "provider", self.instance_id - ) - # migrate old UGP players to this provider - elif player_config.player_id.startswith(UNIVERSAL_PREFIX): - self.mass.config.set_raw_player_config_value( - player_config.player_id, CONF_GROUP_TYPE, "universal" - ) - player_config.provider = self.instance_id - self.mass.config.set_raw_player_config_value( - player_config.player_id, "provider", self.instance_id - ) - - await self._register_all_players() - # listen for player added events so we can catch late joiners - # (because a group depends on its childs to be available) - self._on_unload.append( - self.mass.subscribe(self._on_mass_player_added_event, EventType.PLAYER_ADDED) - ) - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - for unload_cb in self._on_unload: - unload_cb() - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - # default entries for player groups - base_entries = ( - *BASE_PLAYER_CONFIG_ENTRIES, - CONF_ENTRY_PLAYER_ICON_GROUP, - CONF_ENTRY_GROUP_TYPE, - CONF_ENTRY_GROUP_MEMBERS, - ) - # group type is static and can not be changed. we just grab the existing, stored value - group_type: str = self.mass.config.get_raw_player_config_value( - player_id, CONF_GROUP_TYPE, GROUP_TYPE_UNIVERSAL - ) - # handle config entries for universal group players - if group_type == GROUP_TYPE_UNIVERSAL: - group_members = CONF_ENTRY_GROUP_MEMBERS - group_members.options = tuple( - ConfigValueOption(x.display_name, x.player_id) - for x in self.mass.players.all(True, False) - if not x.player_id.startswith(UNIVERSAL_PREFIX) - ) - return ( - *base_entries, - group_members, - CONFIG_ENTRY_UGP_NOTE, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_SAMPLE_RATES_UGP, - CONF_ENTRY_FLOW_MODE_ENFORCED, - ) - # handle config entries for syncgroup players - group_members = CONF_ENTRY_GROUP_MEMBERS - if player_prov := self.mass.get_provider(group_type): - group_members.options = tuple( - ConfigValueOption(x.display_name, x.player_id) for x in player_prov.players - ) - - # grab additional details from one of the provider's players - if not (player_provider := self.mass.get_provider(group_type)): - return base_entries # guard - if TYPE_CHECKING: - player_provider = cast(PlayerProvider, player_provider) - assert player_provider.lookup_key != self.lookup_key - if not (child_player := next((x for x in player_provider.players), None)): - return base_entries # guard - - # combine base group entries with (base) player entries for this player type - allowed_conf_entries = ( - CONF_HTTP_PROFILE, - CONF_ENABLE_ICY_METADATA, - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_ENFORCE_MP3, - CONF_FLOW_MODE, - CONF_SAMPLE_RATES, - ) - child_config_entries = await player_provider.get_player_config_entries( - child_player.player_id - ) - return ( - *base_entries, - group_members, - CONFIG_ENTRY_DYNAMIC_MEMBERS, - *(entry for entry in child_config_entries if entry.key in allowed_conf_entries), - ) - - async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: - """Call (by config manager) when the configuration of a player changes.""" - if f"values/{CONF_GROUP_MEMBERS}" in changed_keys: - members = config.get_value(CONF_GROUP_MEMBERS) - # ensure we filter invalid members - members = self._filter_members(config.get_value(CONF_GROUP_TYPE), members) - if group_player := self.mass.players.get(config.player_id): - group_player.group_childs = members - if group_player.powered: - # power on group player (which will also resync) if needed - await self.cmd_power(group_player.player_id, True) - if f"values/{CONFIG_ENTRY_DYNAMIC_MEMBERS.key}" in changed_keys: - # dynamic members feature changed - if group_player := self.mass.players.get(config.player_id): - if PlayerFeature.SYNC in group_player.supported_features: - group_player.supported_features = tuple( - x for x in group_player.supported_features if x != PlayerFeature.SYNC - ) - else: - group_player.supported_features = ( - *group_player.supported_features, - PlayerFeature.SYNC, - ) - await super().on_player_config_change(config, changed_keys) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - group_player = self.mass.players.get(player_id) - # syncgroup: forward command to sync leader - if player_id.startswith(SYNCGROUP_PREFIX): - if sync_leader := self._get_sync_leader(group_player): - if player_provider := self.mass.get_provider(sync_leader.provider): - await player_provider.cmd_stop(sync_leader.player_id) - return - # ugp: forward command to all members - async with TaskManager(self.mass) as tg: - for member in self.mass.players.iter_group_members(group_player, active_only=True): - if player_provider := self.mass.get_provider(member.provider): - tg.create_task(player_provider.cmd_stop(member.player_id)) - # abort the stream session - if (stream := self.ugp_streams.pop(player_id, None)) and not stream.done: - await stream.stop() - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - group_player = self.mass.players.get(player_id) - if not player_id.startswith(SYNCGROUP_PREFIX): - # this shouldn't happen, but just in case - raise UnsupportedFeaturedException - # forward command to sync leader - if sync_leader := self._get_sync_leader(group_player): - if player_provider := self.mass.get_provider(sync_leader.provider): - await player_provider.cmd_play(sync_leader.player_id) - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - group_player = self.mass.players.get(player_id) - if not player_id.startswith(SYNCGROUP_PREFIX): - # this shouldn't happen, but just in case - raise UnsupportedFeaturedException - # forward command to sync leader - if sync_leader := self._get_sync_leader(group_player): - if player_provider := self.mass.get_provider(sync_leader.provider): - await player_provider.cmd_pause(sync_leader.player_id) - - async def cmd_power(self, player_id: str, powered: bool) -> None: - """Handle POWER command to group player.""" - group_player = self.mass.players.get(player_id, raise_unavailable=True) - if TYPE_CHECKING: - group_player = cast(Player, group_player) - - # always stop at power off - if not powered and group_player.state in (PlayerState.PLAYING, PlayerState.PAUSED): - await self.cmd_stop(group_player.player_id) - - # always (re)fetch the configured group members at power on - if not group_player.powered: - group_member_ids = self.mass.config.get_raw_player_config_value( - player_id, CONF_GROUP_MEMBERS, [] - ) - group_player.group_childs = { - x - for x in group_member_ids - if (child_player := self.mass.players.get(x)) - and child_player.available - and child_player.enabled - } - - if powered: - # handle TURN_ON of the group player by turning on all members - for member in self.mass.players.iter_group_members( - group_player, only_powered=False, active_only=False - ): - player_provider = self.mass.get_provider(member.provider) - assert player_provider # for typing - if ( - member.state in (PlayerState.PLAYING, PlayerState.PAUSED) - and member.active_source != group_player.active_source - ): - # stop playing existing content on member if we start the group player - await player_provider.cmd_stop(member.player_id) - if not member.powered: - member.active_group = None # needed to prevent race conditions - await self.mass.players.cmd_power(member.player_id, True) - # set active source to group player if the group (is going to be) powered - member.active_group = group_player.player_id - member.active_source = group_player.active_source - else: - # handle TURN_OFF of the group player by turning off all members - # optimistically set the group state to prevent race conditions - # with the unsync command - group_player.powered = False - for member in self.mass.players.iter_group_members( - group_player, only_powered=True, active_only=True - ): - # reset active group on player when the group is turned off - member.active_group = None - member.active_source = None - # handle TURN_OFF of the group player by turning off all members - if member.powered: - await self.mass.players.cmd_power(member.player_id, False) - - if powered and player_id.startswith(SYNCGROUP_PREFIX): - await self._sync_syncgroup(group_player) - # optimistically set the group state - group_player.powered = powered - self.mass.players.update(group_player.player_id) - if not powered: - # reset the group members when powered off - group_player.group_childs = set( - self.mass.config.get_raw_player_config_value(player_id, CONF_GROUP_MEMBERS, []) - ) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - # group volume is already handled in the player manager - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - group_player = self.mass.players.get(player_id) - # power on (which will also resync) if needed - await self.cmd_power(player_id, True) - - # handle play_media for sync group - if player_id.startswith(SYNCGROUP_PREFIX): - # simply forward the command to the sync leader - sync_leader = self._select_sync_leader(group_player) - assert sync_leader # for typing - player_provider = self.mass.get_provider(sync_leader.provider) - assert player_provider # for typing - await player_provider.play_media( - sync_leader.player_id, - media=media, - ) - return - - # handle play_media for UGP group - if (existing := self.ugp_streams.pop(player_id, None)) and not existing.done: - # stop any existing stream first - await existing.stop() - - # select audio source - if media.media_type == MediaType.ANNOUNCEMENT: - # special case: stream announcement - audio_source = self.mass.streams.get_announcement_stream( - media.custom_data["url"], - output_format=UGP_FORMAT, - use_pre_announce=media.custom_data["use_pre_announce"], - ) - elif media.queue_id and media.queue_item_id: - # regular queue stream request - audio_source = self.mass.streams.get_flow_stream( - queue=self.mass.player_queues.get(media.queue_id), - start_queue_item=self.mass.player_queues.get_item( - media.queue_id, media.queue_item_id - ), - pcm_format=UGP_FORMAT, - ) - else: - # assume url or some other direct path - # NOTE: this will fail if its an uri not playable by ffmpeg - audio_source = get_ffmpeg_stream( - audio_input=media.uri, - input_format=AudioFormat(ContentType.try_parse(media.uri)), - output_format=UGP_FORMAT, - ) - - # start the stream task - self.ugp_streams[player_id] = UGPStream(audio_source=audio_source, audio_format=UGP_FORMAT) - base_url = f"{self.mass.streams.base_url}/ugp/{player_id}.mp3" - - # set the state optimistically - group_player.current_media = media - group_player.elapsed_time = 0 - group_player.elapsed_time_last_updated = time() - 1 - group_player.state = PlayerState.PLAYING - self.mass.players.update(player_id) - - # forward to downstream play_media commands - async with TaskManager(self.mass) as tg: - for member in self.mass.players.iter_group_members( - group_player, only_powered=True, active_only=True - ): - player_provider = self.mass.get_provider(member.provider) - assert player_provider # for typing - tg.create_task( - player_provider.play_media( - member.player_id, - media=PlayerMedia( - uri=f"{base_url}?player_id={member.player_id}", - media_type=MediaType.FLOW_STREAM, - title=group_player.display_name, - queue_id=group_player.player_id, - ), - ) - ) - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of a next media item on the player.""" - group_player = self.mass.players.get(player_id, True) - if not player_id.startswith(SYNCGROUP_PREFIX): - # this shouldn't happen, but just in case - raise UnsupportedFeaturedException("Command is not supported for UGP players") - if sync_leader := self._get_sync_leader(group_player): - await self.mass.players.enqueue_next_media( - sync_leader.player_id, - media=media, - ) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates. - - This is called by the Player Manager; - if 'needs_poll' is set to True in the player object. - """ - if group_player := self.mass.players.get(player_id): - self._update_attributes(group_player) - - async def create_group(self, group_type: str, name: str, members: list[str]) -> Player: - """Create new Group Player.""" - # perform basic checks - if group_type == GROUP_TYPE_UNIVERSAL: - prefix = UNIVERSAL_PREFIX - else: - prefix = SYNCGROUP_PREFIX - if (player_prov := self.mass.get_provider(group_type)) is None: - msg = f"Provider {group_type} is not available!" - raise ProviderUnavailableError(msg) - if ProviderFeature.SYNC_PLAYERS not in player_prov.supported_features: - msg = f"Provider {player_prov.name} does not support creating groups" - raise UnsupportedFeaturedException(msg) - - new_group_id = f"{prefix}{shortuuid.random(8).lower()}" - # cleanup list, just in case the frontend sends some garbage - members = self._filter_members(group_type, members) - # create default config with the user chosen name - self.mass.config.create_default_player_config( - new_group_id, - self.instance_id, - name=name, - enabled=True, - values={CONF_GROUP_MEMBERS: members, CONF_GROUP_TYPE: group_type}, - ) - return await self._register_group_player( - group_player_id=new_group_id, group_type=group_type, name=name, members=members - ) - - async def remove_player(self, player_id: str) -> None: - """Remove a group player.""" - if not (group_player := self.mass.players.get(player_id)): - return - if group_player.powered: - # edge case: the group player is powered and being removed - # make sure to turn it off first (which will also unsync a syncgroup) - await self.cmd_power(player_id, False) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the sync leader. - """ - group_player = self.mass.players.get(target_player, raise_unavailable=True) - if TYPE_CHECKING: - group_player = cast(Player, group_player) - dynamic_members_enabled = self.mass.config.get_raw_player_config_value( - group_player.player_id, - CONFIG_ENTRY_DYNAMIC_MEMBERS.key, - CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, - ) - group_type = self.mass.config.get_raw_player_config_value( - group_player.player_id, CONF_ENTRY_GROUP_TYPE.key, CONF_ENTRY_GROUP_TYPE.default_value - ) - if not dynamic_members_enabled: - raise UnsupportedFeaturedException( - f"Adjusting group members is not allowed for group {group_player.display_name}" - ) - new_members = self._filter_members(group_type, [*group_player.group_childs, player_id]) - group_player.group_childs = new_members - if group_player.powered: - # power on group player (which will also resync) if needed - await self.cmd_power(target_player, True) - - async def cmd_unsync_member(self, player_id: str, target_player: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player(id) from the given (master) player/sync group. - - - player_id: player_id of the (child) player to unsync from the group. - - target_player: player_id of the group player. - """ - group_player = self.mass.players.get(target_player, raise_unavailable=True) - child_player = self.mass.players.get(player_id, raise_unavailable=True) - if TYPE_CHECKING: - group_player = cast(Player, group_player) - child_player = cast(Player, child_player) - dynamic_members_enabled = self.mass.config.get_raw_player_config_value( - group_player.player_id, - CONFIG_ENTRY_DYNAMIC_MEMBERS.key, - CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, - ) - if group_player.powered and not dynamic_members_enabled: - raise UnsupportedFeaturedException( - f"Adjusting group members is not allowed for group {group_player.display_name}" - ) - is_sync_leader = len(child_player.group_childs) > 0 - was_playing = child_player.state == PlayerState.PLAYING - # forward command to the player provider - if player_provider := self.mass.players.get_player_provider(child_player.player_id): - await player_provider.cmd_unsync(child_player.player_id) - child_player.active_group = None - child_player.active_source = None - group_player.group_childs = {x for x in group_player.group_childs if x != player_id} - if is_sync_leader and was_playing: - # unsyncing the sync leader will stop the group so we need to resume - self.mass.call_later(2, self.mass.players.cmd_play, group_player.player_id) - elif group_player.powered: - # power on group player (which will also resync) if needed - await self.cmd_power(group_player.player_id, True) - - async def _register_all_players(self) -> None: - """Register all (virtual/fake) group players in the Player controller.""" - player_configs = await self.mass.config.get_player_configs( - self.instance_id, include_values=True - ) - for player_config in player_configs: - if self.mass.players.get(player_config.player_id): - continue # already registered - members = player_config.get_value(CONF_GROUP_MEMBERS) - group_type = player_config.get_value(CONF_GROUP_TYPE) - with suppress(PlayerUnavailableError): - await self._register_group_player( - player_config.player_id, - group_type, - player_config.name or player_config.default_name, - members, - ) - - async def _register_group_player( - self, group_player_id: str, group_type: str, name: str, members: Iterable[str] - ) -> Player: - """Register a syncgroup player.""" - player_features = {PlayerFeature.POWER, PlayerFeature.VOLUME_SET} - - if not (self.mass.players.get(x) for x in members): - raise PlayerUnavailableError("One or more members are not available!") - - if group_type == GROUP_TYPE_UNIVERSAL: - model_name = "Universal Group" - manufacturer = self.name - # register dynamic route for the ugp stream - route_path = f"/ugp/{group_player_id}.mp3" - self._on_unload.append( - self.mass.streams.register_dynamic_route(route_path, self._serve_ugp_stream) - ) - elif player_provider := self.mass.get_provider(group_type): - # grab additional details from one of the provider's players - if TYPE_CHECKING: - player_provider = cast(PlayerProvider, player_provider) - model_name = "Sync Group" - manufacturer = self.mass.get_provider(group_type).name - for feature in (PlayerFeature.PAUSE, PlayerFeature.VOLUME_MUTE, PlayerFeature.ENQUEUE): - if all(feature in x.supported_features for x in player_provider.players): - player_features.add(feature) - else: - raise PlayerUnavailableError(f"Provider for syncgroup {group_type} is not available!") - - if self.mass.config.get_raw_player_config_value( - group_player_id, - CONFIG_ENTRY_DYNAMIC_MEMBERS.key, - CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, - ): - player_features.add(PlayerFeature.SYNC) - - player = Player( - player_id=group_player_id, - provider=self.instance_id, - type=PlayerType.GROUP, - name=name, - available=True, - powered=False, - device_info=DeviceInfo(model=model_name, manufacturer=manufacturer), - supported_features=tuple(player_features), - group_childs=set(members), - active_source=group_player_id, - needs_poll=True, - poll_interval=30, - ) - - await self.mass.players.register_or_update(player) - self._update_attributes(player) - return player - - def _get_sync_leader(self, group_player: Player) -> Player | None: - """Get the active sync leader player for the syncgroup.""" - if group_player.synced_to: - # should not happen but just in case... - return self.mass.players.get(group_player.synced_to) - if len(group_player.group_childs) == 1: - # Return the (first/only) player - # this is to handle the edge case where players are not - # yet synced or there simply is just one player - for child_player in self.mass.players.iter_group_members( - group_player, only_powered=False, only_playing=False, active_only=False - ): - if not child_player.synced_to: - return child_player - # Return the (first/only) player that has group childs - for child_player in self.mass.players.iter_group_members( - group_player, only_powered=False, only_playing=False, active_only=False - ): - if child_player.group_childs: - return child_player - return None - - def _select_sync_leader(self, group_player: Player) -> Player | None: - """Select the active sync leader player for a syncgroup.""" - if sync_leader := self._get_sync_leader(group_player): - return sync_leader - # select new sync leader: return the first active player - for child_player in self.mass.players.iter_group_members(group_player, active_only=True): - if child_player.active_group not in (None, group_player.player_id): - continue - if ( - child_player.active_source - and child_player.active_source != group_player.active_source - ): - continue - return child_player - # fallback select new sync leader: simply return the first (available) player - for child_player in self.mass.players.iter_group_members( - group_player, only_powered=False, only_playing=False, active_only=False - ): - return child_player - # this really should not be possible - raise RuntimeError("No players available to form syncgroup") - - async def _sync_syncgroup(self, group_player: Player) -> None: - """Sync all (possible) players of a syncgroup.""" - sync_leader = self._select_sync_leader(group_player) - members_to_sync: list[str] = [] - for member in self.mass.players.iter_group_members(group_player, active_only=False): - if member.synced_to and member.synced_to != sync_leader.player_id: - # unsync first - await self.mass.players.cmd_unsync(member.player_id) - if sync_leader.player_id == member.player_id: - # skip sync leader - continue - if ( - member.synced_to == sync_leader.player_id - and member.player_id in sync_leader.group_childs - ): - # already synced - continue - members_to_sync.append(member.player_id) - if members_to_sync: - await self.mass.players.cmd_sync_many(sync_leader.player_id, members_to_sync) - - async def _on_mass_player_added_event(self, event: MassEvent) -> None: - """Handle player added event from player controller.""" - await self._register_all_players() - - def _update_attributes(self, player: Player) -> None: - """Update attributes of a player.""" - for child_player in self.mass.players.iter_group_members(player, active_only=True): - # just grab the first active player - if child_player.synced_to: - continue - player.state = child_player.state - if child_player.current_media: - player.current_media = child_player.current_media - player.elapsed_time = child_player.elapsed_time - player.elapsed_time_last_updated = child_player.elapsed_time_last_updated - break - else: - player.state = PlayerState.IDLE - player.active_source = player.player_id - self.mass.players.update(player.player_id) - - async def _serve_ugp_stream(self, request: web.Request) -> web.Response: - """Serve the UGP (multi-client) flow stream audio to a player.""" - ugp_player_id = request.path.rsplit(".")[0].rsplit("/")[-1] - child_player_id = request.query.get("player_id") # optional! - - if not (ugp_player := self.mass.players.get(ugp_player_id)): - raise web.HTTPNotFound(reason=f"Unknown UGP player: {ugp_player_id}") - - if not (stream := self.ugp_streams.get(ugp_player_id, None)) or stream.done: - raise web.HTTPNotFound(body=f"There is no active UGP stream for {ugp_player_id}!") - - http_profile: str = await self.mass.config.get_player_config_value( - child_player_id, CONF_HTTP_PROFILE - ) - headers = { - **DEFAULT_STREAM_HEADERS, - "Content-Type": "audio/mp3", - "Accept-Ranges": "none", - "Cache-Control": "no-cache", - "Connection": "close", - } - - resp = web.StreamResponse(status=200, reason="OK", headers=headers) - if http_profile == "forced_content_length": - resp.content_length = 4294967296 - elif http_profile == "chunked": - resp.enable_chunked_encoding() - - await resp.prepare(request) - - # return early if this is not a GET request - if request.method != "GET": - return resp - - # all checks passed, start streaming! - self.logger.debug( - "Start serving UGP flow audio stream for UGP-player %s to %s", - ugp_player.display_name, - child_player_id or request.remote, - ) - async for chunk in stream.subscribe(): - try: - await resp.write(chunk) - except (ConnectionError, ConnectionResetError): - break - - return resp - - def _filter_members(self, group_type: str, members: list[str]) -> list[str]: - """Filter out members that are not valid players.""" - if group_type != GROUP_TYPE_UNIVERSAL: - player_provider = self.mass.get_provider(group_type) - return [ - x - for x in members - if (player := self.mass.players.get(x)) - and player.provider in (player_provider.instance_id, self.instance_id) - ] - # cleanup members - filter out impossible choices - syncgroup_childs: list[str] = [] - for member in members: - if not member.startswith(SYNCGROUP_PREFIX): - continue - if syncgroup := self.mass.players.get(member): - syncgroup_childs.extend(syncgroup.group_childs) - # we filter out other UGP players and syncgroup childs - # if their parent is already in the list - return [ - x - for x in members - if self.mass.players.get(x) - and x not in syncgroup_childs - and not x.startswith(UNIVERSAL_PREFIX) - ] diff --git a/music_assistant/server/providers/player_group/manifest.json b/music_assistant/server/providers/player_group/manifest.json deleted file mode 100644 index 9c2da78e..00000000 --- a/music_assistant/server/providers/player_group/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "player", - "domain": "player_group", - "name": "Playergroup", - "description": "Create (permanent) groups of your favorite players. \nSupports both syncgroups (to group speakers of the same ecocystem to play in sync) and universal groups to group speakers of different ecosystems to play the same audio (but not in sync).", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/faq/groups/", - "multi_instance": false, - "builtin": true, - "allow_disable": false, - "icon": "speaker-multiple" -} diff --git a/music_assistant/server/providers/player_group/ugp_stream.py b/music_assistant/server/providers/player_group/ugp_stream.py deleted file mode 100644 index 281d80fb..00000000 --- a/music_assistant/server/providers/player_group/ugp_stream.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Implementation of a Stream for the Universal Group Player. - -Basically this is like a fake radio radio stream (MP3) format with multiple subscribers. -The MP3 format is chosen because it is widely supported. -""" - -from __future__ import annotations - -import asyncio -from collections.abc import AsyncGenerator, Awaitable, Callable - -from music_assistant.common.helpers.util import empty_queue -from music_assistant.common.models.enums import ContentType -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.server.helpers.audio import get_ffmpeg_stream - -# ruff: noqa: ARG002 - -UGP_FORMAT = AudioFormat( - content_type=ContentType.PCM_F32LE, - sample_rate=48000, - bit_depth=32, -) - - -class UGPStream: - """ - Implementation of a Stream for the Universal Group Player. - - Basically this is like a fake radio radio stream (MP3) format with multiple subscribers. - The MP3 format is chosen because it is widely supported. - """ - - def __init__( - self, - audio_source: AsyncGenerator[bytes, None], - audio_format: AudioFormat, - ) -> None: - """Initialize UGP Stream.""" - self.audio_source = audio_source - self.input_format = audio_format - self.output_format = AudioFormat(content_type=ContentType.MP3) - self.subscribers: list[Callable[[bytes], Awaitable]] = [] - self._task: asyncio.Task | None = None - self._done: asyncio.Event = asyncio.Event() - - @property - def done(self) -> bool: - """Return if this stream is already done.""" - return self._done.is_set() and self._task and self._task.done() - - async def stop(self) -> None: - """Stop/cancel the stream.""" - if self._done.is_set(): - return - if self._task and not self._task.done(): - self._task.cancel() - self._done.set() - - async def subscribe(self) -> AsyncGenerator[bytes, None]: - """Subscribe to the raw/unaltered audio stream.""" - # start the runner as soon as the (first) client connects - if not self._task: - self._task = asyncio.create_task(self._runner()) - queue = asyncio.Queue(10) - try: - self.subscribers.append(queue.put) - while True: - chunk = await queue.get() - if not chunk: - break - yield chunk - finally: - self.subscribers.remove(queue.put) - empty_queue(queue) - del queue - - async def _runner(self) -> None: - """Run the stream for the given audio source.""" - await asyncio.sleep(0.25) # small delay to allow subscribers to connect - async for chunk in get_ffmpeg_stream( - audio_input=self.audio_source, - input_format=self.input_format, - output_format=self.output_format, - # we don't allow the player to buffer too much ahead so we use readrate limiting - extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"], - ): - await asyncio.gather( - *[sub(chunk) for sub in self.subscribers], - return_exceptions=True, - ) - # empty chunk when done - await asyncio.gather(*[sub(b"") for sub in self.subscribers], return_exceptions=True) - self._done.set() diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py deleted file mode 100644 index 748b65bf..00000000 --- a/music_assistant/server/providers/plex/__init__.py +++ /dev/null @@ -1,967 +0,0 @@ -"""Plex musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import logging -from asyncio import Task, TaskGroup -from collections.abc import Awaitable -from contextlib import suppress -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast - -import plexapi.exceptions -import requests -from plexapi.audio import Album as PlexAlbum -from plexapi.audio import Artist as PlexArtist -from plexapi.audio import Playlist as PlexPlaylist -from plexapi.audio import Track as PlexTrack -from plexapi.base import PlexObject -from plexapi.myplex import MyPlexAccount, MyPlexPinLogin -from plexapi.server import PlexServer - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - InvalidDataError, - LoginFailed, - MediaNotFoundError, - SetupFailedError, -) -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - ItemMapping, - MediaItem, - MediaItemChapter, - MediaItemImage, - Playlist, - ProviderMapping, - SearchResults, - Track, - UniqueList, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import UNKNOWN_ARTIST -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.providers.plex.helpers import discover_local_servers, get_libraries - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable, Coroutine - - from plexapi.library import MusicSection as PlexMusicSection - from plexapi.media import AudioStream as PlexAudioStream - from plexapi.media import Media as PlexMedia - from plexapi.media import MediaPart as PlexMediaPart - - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -CONF_ACTION_AUTH_MYPLEX = "auth_myplex" -CONF_ACTION_AUTH_LOCAL = "auth_local" -CONF_ACTION_CLEAR_AUTH = "auth" -CONF_ACTION_LIBRARY = "library" -CONF_ACTION_GDM = "gdm" - -CONF_AUTH_TOKEN = "token" -CONF_LIBRARY_ID = "library_id" -CONF_LOCAL_SERVER_IP = "local_server_ip" -CONF_LOCAL_SERVER_PORT = "local_server_port" -CONF_LOCAL_SERVER_SSL = "local_server_ssl" -CONF_LOCAL_SERVER_VERIFY_CERT = "local_server_verify_cert" - -FAKE_ARTIST_PREFIX = "_fake://" - -AUTH_TOKEN_UNAUTH = "local_auth" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - if not config.get_value(CONF_AUTH_TOKEN): - msg = "Invalid login credentials" - raise LoginFailed(msg) - - return PlexProvider(mass, manifest, config) - - -async def get_config_entries( # noqa: PLR0915 - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # handle action GDM discovery - if action == CONF_ACTION_GDM: - server_details = await discover_local_servers() - if server_details and server_details[0] and server_details[1]: - assert values - values[CONF_LOCAL_SERVER_IP] = server_details[0] - values[CONF_LOCAL_SERVER_PORT] = server_details[1] - values[CONF_LOCAL_SERVER_SSL] = False - values[CONF_LOCAL_SERVER_VERIFY_CERT] = False - else: - assert values - values[CONF_LOCAL_SERVER_IP] = "Discovery failed, please add IP manually" - values[CONF_LOCAL_SERVER_PORT] = 32400 - values[CONF_LOCAL_SERVER_SSL] = False - values[CONF_LOCAL_SERVER_VERIFY_CERT] = True - - # handle action clear authentication - if action == CONF_ACTION_CLEAR_AUTH: - assert values - values[CONF_AUTH_TOKEN] = None - values[CONF_LOCAL_SERVER_IP] = None - values[CONF_LOCAL_SERVER_PORT] = 32400 - values[CONF_LOCAL_SERVER_SSL] = False - values[CONF_LOCAL_SERVER_VERIFY_CERT] = True - - # handle action MyPlex auth - if action == CONF_ACTION_AUTH_MYPLEX: - assert values - values[CONF_AUTH_TOKEN] = None - async with AuthenticationHelper(mass, str(values["session_id"])) as auth_helper: - plex_auth = MyPlexPinLogin(headers={"X-Plex-Product": "Music Assistant"}, oauth=True) - auth_url = plex_auth.oauthUrl(auth_helper.callback_url) - await auth_helper.authenticate(auth_url) - if not plex_auth.checkLogin(): - msg = "Authentication to MyPlex failed" - raise LoginFailed(msg) - # set the retrieved token on the values object to pass along - values[CONF_AUTH_TOKEN] = plex_auth.token - - # handle action Local auth (no MyPlex) - if action == CONF_ACTION_AUTH_LOCAL: - assert values - values[CONF_AUTH_TOKEN] = AUTH_TOKEN_UNAUTH - - # collect all config entries to show - entries: list[ConfigEntry] = [] - - # show GDM discovery (if we do not yet have any server details) - if values is None or not values.get(CONF_LOCAL_SERVER_IP): - entries.append( - ConfigEntry( - key=CONF_ACTION_GDM, - type=ConfigEntryType.ACTION, - label="Use Plex GDM to discover local servers", - description='Enable "GDM" to discover local Plex servers automatically.', - action=CONF_ACTION_GDM, - action_label="Use Plex GDM to discover local servers", - ) - ) - - # server details config entries (IP, port etc.) - entries += [ - ConfigEntry( - key=CONF_LOCAL_SERVER_IP, - type=ConfigEntryType.STRING, - label="Local server IP", - description="The local server IP (e.g. 192.168.1.77)", - required=True, - value=values.get(CONF_LOCAL_SERVER_IP) if values else None, - ), - ConfigEntry( - key=CONF_LOCAL_SERVER_PORT, - type=ConfigEntryType.INTEGER, - label="Local server port", - description="The local server port (e.g. 32400)", - required=True, - default_value=32400, - value=values.get(CONF_LOCAL_SERVER_PORT) if values else None, - ), - ConfigEntry( - key=CONF_LOCAL_SERVER_SSL, - type=ConfigEntryType.BOOLEAN, - label="SSL (HTTPS)", - description="Connect to the local server using SSL (HTTPS)", - required=True, - default_value=False, - ), - ConfigEntry( - key=CONF_LOCAL_SERVER_VERIFY_CERT, - type=ConfigEntryType.BOOLEAN, - label="Verify certificate", - description="Verify local server SSL certificate", - required=True, - default_value=True, - depends_on=CONF_LOCAL_SERVER_SSL, - category="advanced", - ), - ConfigEntry( - key=CONF_AUTH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label=CONF_AUTH_TOKEN, - action=CONF_AUTH_TOKEN, - value=values.get(CONF_AUTH_TOKEN) if values else None, - hidden=True, - ), - ] - - # config flow auth action/step to pick the library to use - # because this call is very slow, we only show/calculate the dropdown if we do - # not yet have this info or we/user invalidated it. - if values and values.get(CONF_AUTH_TOKEN): - conf_libraries = ConfigEntry( - key=CONF_LIBRARY_ID, - type=ConfigEntryType.STRING, - label="Library", - required=True, - description="The library to connect to (e.g. Music)", - depends_on=CONF_AUTH_TOKEN, - action=CONF_ACTION_LIBRARY, - action_label="Select Plex Music Library", - ) - if action in (CONF_ACTION_LIBRARY, CONF_ACTION_AUTH_MYPLEX, CONF_ACTION_AUTH_LOCAL): - token = mass.config.decrypt_string(str(values.get(CONF_AUTH_TOKEN))) - server_http_ip = str(values.get(CONF_LOCAL_SERVER_IP)) - server_http_port = str(values.get(CONF_LOCAL_SERVER_PORT)) - server_http_ssl = bool(values.get(CONF_LOCAL_SERVER_SSL)) - server_http_verify_cert = bool(values.get(CONF_LOCAL_SERVER_VERIFY_CERT)) - if not ( - libraries := await get_libraries( - mass, - token, - server_http_ssl, - server_http_ip, - server_http_port, - server_http_verify_cert, - ) - ): - msg = "Unable to retrieve Servers and/or Music Libraries" - raise LoginFailed(msg) - conf_libraries.options = tuple( - # use the same value for both the value and the title - # until we find out what plex uses as stable identifiers - ConfigValueOption( - title=x, - value=x, - ) - for x in libraries - ) - # select first library as (default) value - conf_libraries.default_value = libraries[0] - conf_libraries.value = libraries[0] - entries.append(conf_libraries) - - # show authentication options - if values is None or not values.get(CONF_AUTH_TOKEN): - entries.append( - ConfigEntry( - key=CONF_ACTION_AUTH_MYPLEX, - type=ConfigEntryType.ACTION, - label="Authenticate with MyPlex", - description="Authenticate with MyPlex to access your library.", - action=CONF_ACTION_AUTH_MYPLEX, - action_label="Authenticate with MyPlex", - ) - ) - entries.append( - ConfigEntry( - key=CONF_ACTION_AUTH_LOCAL, - type=ConfigEntryType.ACTION, - label="Authenticate locally", - description="Authenticate locally to access your library.", - action=CONF_ACTION_AUTH_LOCAL, - action_label="Authenticate locally", - ) - ) - else: - entries.append( - ConfigEntry( - key=CONF_ACTION_CLEAR_AUTH, - type=ConfigEntryType.ACTION, - label="Clear authentication", - description="Clear the current authentication details.", - action=CONF_ACTION_CLEAR_AUTH, - action_label="Clear authentication", - required=False, - ) - ) - - # return all config entries - return tuple(entries) - - -Param = ParamSpec("Param") -RetType = TypeVar("RetType") -PlexObjectT = TypeVar("PlexObjectT", bound=PlexObject) -MediaItemT = TypeVar("MediaItemT", bound=MediaItem) - - -class PlexProvider(MusicProvider): - """Provider for a plex music library.""" - - _plex_server: PlexServer = None - _plex_library: PlexMusicSection = None - _myplex_account: MyPlexAccount = None - _baseurl: str - - async def handle_async_init(self) -> None: - """Set up the music provider by connecting to the server.""" - # silence loggers - logging.getLogger("plexapi").setLevel(self.logger.level + 10) - _, library_name = str(self.config.get_value(CONF_LIBRARY_ID)).split(" / ", 1) - - def connect() -> PlexServer: - try: - session = requests.Session() - session.verify = ( - self.config.get_value(CONF_LOCAL_SERVER_VERIFY_CERT) - if self.config.get_value(CONF_LOCAL_SERVER_SSL) - else False - ) - local_server_protocol = ( - "https" if self.config.get_value(CONF_LOCAL_SERVER_SSL) else "http" - ) - token = self.config.get_value(CONF_AUTH_TOKEN) - plex_url = ( - f"{local_server_protocol}://{self.config.get_value(CONF_LOCAL_SERVER_IP)}" - f":{self.config.get_value(CONF_LOCAL_SERVER_PORT)}" - ) - if token == AUTH_TOKEN_UNAUTH: - # Doing local connection, not via plex.tv. - plex_server = PlexServer(plex_url) - else: - plex_server = PlexServer( - plex_url, - token, - session=session, - ) - # I don't think PlexAPI intends for this to be accessible, but we need it. - self._baseurl = plex_server._baseurl - - except plexapi.exceptions.BadRequest as err: - if "Invalid token" in str(err): - # token invalid, invalidate the config - self.mass.create_task( - self.mass.config.remove_provider_config_value( - self.instance_id, CONF_AUTH_TOKEN - ), - ) - msg = "Authentication failed" - raise LoginFailed(msg) - raise LoginFailed from err - return plex_server - - self._myplex_account = await self.get_myplex_account_and_refresh_token( - str(self.config.get_value(CONF_AUTH_TOKEN)) - ) - try: - self._plex_server = await self._run_async(connect) - self._plex_library = await self._run_async( - self._plex_server.library.section, library_name - ) - except requests.exceptions.ConnectionError as err: - raise SetupFailedError from err - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return a list of supported features.""" - return ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ) - - @property - def is_streaming_provider(self) -> bool: - """ - Return True if the provider is a streaming provider. - - This literally means that the catalog is not the same as the library contents. - For local based providers (files, plex), the catalog is the same as the library content. - It also means that data is if this provider is NOT a streaming provider, - data cross instances is unique, the catalog and library differs per instance. - - Setting this to True will only query one instance of the provider for search and lookups. - Setting this to False will query all instances of this provider for search and lookups. - """ - return False - - async def resolve_image(self, path: str) -> str | bytes: - """Return the full image URL including the auth token.""" - return str(self._plex_server.url(path, True)) - - async def _run_async( - self, call: Callable[Param, RetType], *args: Param.args, **kwargs: Param.kwargs - ) -> RetType: - await self.get_myplex_account_and_refresh_token(str(self.config.get_value(CONF_AUTH_TOKEN))) - return await asyncio.to_thread(call, *args, **kwargs) - - async def _get_data(self, key: str, cls: type[PlexObjectT]) -> PlexObjectT: - results = await self._run_async(self._plex_library.fetchItem, key, cls) - return cast(PlexObjectT, results) - - def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: - name, version = parse_title_and_version(name) - if media_type in (MediaType.ALBUM, MediaType.TRACK): - name, version = parse_title_and_version(name) - else: - version = "" - return ItemMapping( - media_type=media_type, - item_id=key, - provider=self.instance_id, - name=name, - version=version, - ) - - async def _get_or_create_artist_by_name(self, artist_name: str) -> Artist | ItemMapping: - if library_items := await self.mass.music.artists._get_library_items_by_query( - search=artist_name, provider=self.instance_id - ): - return ItemMapping.from_item(library_items[0]) - - artist_id = FAKE_ARTIST_PREFIX + artist_name - return Artist( - item_id=artist_id, - name=artist_name or UNKNOWN_ARTIST, - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - - async def _parse(self, plex_media: PlexObject) -> MediaItem | None: - if plex_media.type == "artist": - return await self._parse_artist(plex_media) - elif plex_media.type == "album": - return await self._parse_album(plex_media) - elif plex_media.type == "track": - return await self._parse_track(plex_media) - elif plex_media.type == "playlist": - return await self._parse_playlist(plex_media) - return None - - async def _search_track(self, search_query: str | None, limit: int) -> list[PlexTrack]: - return cast( - list[PlexTrack], - await self._run_async(self._plex_library.searchTracks, title=search_query, limit=limit), - ) - - async def _search_album(self, search_query: str, limit: int) -> list[PlexAlbum]: - return cast( - list[PlexAlbum], - await self._run_async(self._plex_library.searchAlbums, title=search_query, limit=limit), - ) - - async def _search_artist(self, search_query: str, limit: int) -> list[PlexArtist]: - return cast( - list[PlexArtist], - await self._run_async( - self._plex_library.searchArtists, title=search_query, limit=limit - ), - ) - - async def _search_playlist(self, search_query: str, limit: int) -> list[PlexPlaylist]: - return cast( - list[PlexPlaylist], - await self._run_async(self._plex_library.playlists, title=search_query, limit=limit), - ) - - async def _search_track_advanced(self, limit: int, **kwargs: Any) -> list[PlexTrack]: - return cast( - list[PlexPlaylist], - await self._run_async(self._plex_library.searchTracks, filters=kwargs, limit=limit), - ) - - async def _search_album_advanced(self, limit: int, **kwargs: Any) -> list[PlexAlbum]: - return cast( - list[PlexPlaylist], - await self._run_async(self._plex_library.searchAlbums, filters=kwargs, limit=limit), - ) - - async def _search_artist_advanced(self, limit: int, **kwargs: Any) -> list[PlexArtist]: - return cast( - list[PlexPlaylist], - await self._run_async(self._plex_library.searchArtists, filters=kwargs, limit=limit), - ) - - async def _search_playlist_advanced(self, limit: int, **kwargs: Any) -> list[PlexPlaylist]: - return cast( - list[PlexPlaylist], - await self._run_async(self._plex_library.playlists, filters=kwargs, limit=limit), - ) - - async def _search_and_parse( - self, - search_coro: Awaitable[list[PlexObjectT]], - parse_coro: Callable[[PlexObjectT], Coroutine[Any, Any, MediaItemT]], - ) -> list[MediaItemT]: - task_results: list[Task[MediaItemT]] = [] - async with TaskGroup() as tg: - for item in await search_coro: - task_results.append(tg.create_task(parse_coro(item))) - - results: list[MediaItemT] = [] - for task in task_results: - results.append(task.result()) - - return results - - async def _parse_album(self, plex_album: PlexAlbum) -> Album: - """Parse a Plex Album response to an Album model object.""" - album_id = plex_album.key - album = Album( - item_id=album_id, - provider=self.domain, - name=plex_album.title or "[Unknown]", - provider_mappings={ - ProviderMapping( - item_id=str(album_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=plex_album.getWebURL(self._baseurl), - ) - }, - ) - # Only add 5-star rated albums to Favorites. rating will be 10.0 for those. - # TODO: Let user set threshold? - with suppress(KeyError): - # suppress KeyError (as it doesn't exist for items without rating), - # allow sync to continue - album.favorite = plex_album._data.attrib["userRating"] == "10.0" - - if plex_album.year: - album.year = plex_album.year - if thumb := plex_album.firstAttr("thumb", "parentThumb", "grandparentThumb"): - album.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=thumb, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - ) - if plex_album.summary: - album.metadata.description = plex_album.summary - - album.artists.append( - self._get_item_mapping( - MediaType.ARTIST, - plex_album.parentKey, - plex_album.parentTitle or UNKNOWN_ARTIST, - ) - ) - return album - - async def _parse_artist(self, plex_artist: PlexArtist) -> Artist: - """Parse a Plex Artist response to Artist model object.""" - artist_id = plex_artist.key - if not artist_id: - msg = "Artist does not have a valid ID" - raise InvalidDataError(msg) - artist = Artist( - item_id=artist_id, - name=plex_artist.title or UNKNOWN_ARTIST, - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=plex_artist.getWebURL(self._baseurl), - ) - }, - ) - if plex_artist.summary: - artist.metadata.description = plex_artist.summary - if thumb := plex_artist.firstAttr("thumb", "parentThumb", "grandparentThumb"): - artist.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=thumb, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - ) - return artist - - async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist: - """Parse a Plex Playlist response to a Playlist object.""" - playlist = Playlist( - item_id=plex_playlist.key, - provider=self.domain, - name=plex_playlist.title or "[Unknown]", - provider_mappings={ - ProviderMapping( - item_id=plex_playlist.key, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=plex_playlist.getWebURL(self._baseurl), - ) - }, - ) - if plex_playlist.summary: - playlist.metadata.description = plex_playlist.summary - if thumb := plex_playlist.firstAttr("thumb", "parentThumb", "grandparentThumb"): - playlist.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=thumb, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - ) - playlist.is_editable = not plex_playlist.smart - playlist.cache_checksum = str(plex_playlist.updatedAt.timestamp()) - - return playlist - - async def _parse_track(self, plex_track: PlexTrack) -> Track: - """Parse a Plex Track response to a Track model object.""" - if plex_track.media: - available = True - content = plex_track.media[0].container - else: - available = False - content = None - track = Track( - item_id=plex_track.key, - provider=self.instance_id, - name=plex_track.title or "[Unknown]", - provider_mappings={ - ProviderMapping( - item_id=plex_track.key, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=available, - audio_format=AudioFormat( - content_type=( - ContentType.try_parse(content) if content else ContentType.UNKNOWN - ), - ), - url=plex_track.getWebURL(self._baseurl), - ) - }, - disc_number=plex_track.parentIndex or 0, - track_number=plex_track.trackNumber or 0, - ) - # Only add 5-star rated tracks to Favorites. userRating will be 10.0 for those. - # TODO: Let user set threshold? - with suppress(KeyError): - # suppress KeyError (as it doesn't exist for items without rating), - # allow sync to continue - track.favorite = plex_track._data.attrib["userRating"] == "10.0" - - if plex_track.originalTitle and plex_track.originalTitle != plex_track.grandparentTitle: - # The artist of the track if different from the album's artist. - # For this kind of artist, we just know the name, so we create a fake artist, - # if it does not already exist. - track.artists.append( - await self._get_or_create_artist_by_name(plex_track.originalTitle or UNKNOWN_ARTIST) - ) - elif plex_track.grandparentKey: - track.artists.append( - self._get_item_mapping( - MediaType.ARTIST, - plex_track.grandparentKey, - plex_track.grandparentTitle or UNKNOWN_ARTIST, - ) - ) - else: - msg = "No artist was found for track" - raise InvalidDataError(msg) - - if thumb := plex_track.firstAttr("thumb", "parentThumb", "grandparentThumb"): - track.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=thumb, - provider=self.instance_id, - remotely_accessible=False, - ) - ] - ) - if plex_track.parentKey: - track.album = self._get_item_mapping( - MediaType.ALBUM, plex_track.parentKey, plex_track.parentTitle - ) - if plex_track.duration: - track.duration = int(plex_track.duration / 1000) - if plex_track.chapters: - track.metadata.chapters = UniqueList( - [ - MediaItemChapter( - chapter_id=plex_chapter.id, - position_start=plex_chapter.start, - position_end=plex_chapter.end, - title=plex_chapter.title, - ) - for plex_chapter in plex_track.chapters - ] - ) - - return track - - async def search( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 20, - ) -> SearchResults: - """Perform search on the plex library. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - artists = None - albums = None - tracks = None - playlists = None - - async with TaskGroup() as tg: - if MediaType.ARTIST in media_types: - artists = tg.create_task( - self._search_and_parse( - self._search_artist(search_query, limit), self._parse_artist - ) - ) - - if MediaType.ALBUM in media_types: - albums = tg.create_task( - self._search_and_parse( - self._search_album(search_query, limit), self._parse_album - ) - ) - - if MediaType.TRACK in media_types: - tracks = tg.create_task( - self._search_and_parse( - self._search_track(search_query, limit), self._parse_track - ) - ) - - if MediaType.PLAYLIST in media_types: - playlists = tg.create_task( - self._search_and_parse( - self._search_playlist(search_query, limit), - self._parse_playlist, - ) - ) - - search_results = SearchResults() - - if artists: - search_results.artists = artists.result() - - if albums: - search_results.albums = albums.result() - - if tracks: - search_results.tracks = tracks.result() - - if playlists: - search_results.playlists = playlists.result() - - return search_results - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Plex Music.""" - artists_obj = await self._run_async(self._plex_library.all) - for artist in artists_obj: - yield await self._parse_artist(artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Plex Music.""" - albums_obj = await self._run_async(self._plex_library.albums) - for album in albums_obj: - yield await self._parse_album(album) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from the provider.""" - playlists_obj = await self._run_async(self._plex_library.playlists) - for playlist in playlists_obj: - yield await self._parse_playlist(playlist) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Plex Music.""" - tracks_obj = await self._search_track(None, limit=99999) - for track in tracks_obj: - yield await self._parse_track(track) - - async def get_album(self, prov_album_id: str) -> Album: - """Get full album details by id.""" - if plex_album := await self._get_data(prov_album_id, PlexAlbum): - return await self._parse_album(plex_album) - msg = f"Item {prov_album_id} not found" - raise MediaNotFoundError(msg) - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get album tracks for given album id.""" - plex_album: PlexAlbum = await self._get_data(prov_album_id, PlexAlbum) - tracks = [] - for plex_track in await self._run_async(plex_album.tracks): - track = await self._parse_track( - plex_track, - ) - tracks.append(track) - return tracks - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - if prov_artist_id.startswith(FAKE_ARTIST_PREFIX): - # This artist does not exist in plex, so we can just load it from DB. - - if db_artist := await self.mass.music.artists.get_library_item_by_prov_id( - prov_artist_id, self.instance_id - ): - return db_artist - msg = f"Artist not found: {prov_artist_id}" - raise MediaNotFoundError(msg) - - if plex_artist := await self._get_data(prov_artist_id, PlexArtist): - return await self._parse_artist(plex_artist) - msg = f"Item {prov_artist_id} not found" - raise MediaNotFoundError(msg) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - if plex_track := await self._get_data(prov_track_id, PlexTrack): - return await self._parse_track(plex_track) - msg = f"Item {prov_track_id} not found" - raise MediaNotFoundError(msg) - - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get full playlist details by id.""" - if plex_playlist := await self._get_data(prov_playlist_id, PlexPlaylist): - return await self._parse_playlist(plex_playlist) - msg = f"Item {prov_playlist_id} not found" - raise MediaNotFoundError(msg) - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # paging not supported, we always return the whole list at once - return [] - plex_playlist: PlexPlaylist = await self._get_data(prov_playlist_id, PlexPlaylist) - if not (playlist_items := await self._run_async(plex_playlist.items)): - return result - for index, plex_track in enumerate(playlist_items, 1): - if track := await self._parse_track(plex_track): - track.position = index - result.append(track) - return result - - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get a list of albums for the given artist.""" - if not prov_artist_id.startswith(FAKE_ARTIST_PREFIX): - plex_artist = await self._get_data(prov_artist_id, PlexArtist) - plex_albums = cast(list[PlexAlbum], await self._run_async(plex_artist.albums)) - if plex_albums: - albums = [] - for album_obj in plex_albums: - albums.append(await self._parse_album(album_obj)) - return albums - return [] - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track.""" - plex_track = await self._get_data(item_id, PlexTrack) - if not plex_track or not plex_track.media: - msg = f"track {item_id} not found" - raise MediaNotFoundError(msg) - - media: PlexMedia = plex_track.media[0] - - media_type = ( - ContentType.try_parse(media.container) if media.container else ContentType.UNKNOWN - ) - media_part: PlexMediaPart = media.parts[0] - audio_stream: PlexAudioStream = media_part.audioStreams()[0] - - stream_details = StreamDetails( - item_id=plex_track.key, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=media_type, - channels=media.audioChannels, - ), - stream_type=StreamType.HTTP, - duration=plex_track.duration, - data=plex_track, - ) - - if media_type != ContentType.M4A: - stream_details.path = self._plex_server.url(media_part.key, True) - if audio_stream.samplingRate: - stream_details.audio_format.sample_rate = audio_stream.samplingRate - if audio_stream.bitDepth: - stream_details.audio_format.bit_depth = audio_stream.bitDepth - - else: - url = plex_track.getStreamURL() - media_info = await parse_tags(url) - stream_details.path = url - stream_details.audio_format.channels = media_info.channels - stream_details.audio_format.content_type = ContentType.try_parse(media_info.format) - stream_details.audio_format.sample_rate = media_info.sample_rate - stream_details.audio_format.bit_depth = media_info.bits_per_sample - - return stream_details - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - - def mark_played() -> None: - item = streamdetails.data - params = {"key": str(item.ratingKey), "identifier": "com.plexapp.plugins.library"} - self._plex_server.query("/:/scrobble", params=params) - - await asyncio.to_thread(mark_played) - - async def get_myplex_account_and_refresh_token(self, auth_token: str) -> MyPlexAccount: - """Get a MyPlexAccount object and refresh the token if needed.""" - if auth_token == AUTH_TOKEN_UNAUTH: - return self._myplex_account - - def _refresh_plex_token() -> MyPlexAccount: - if self._myplex_account is None: - myplex_account = MyPlexAccount(token=auth_token) - self._myplex_account = myplex_account - self._myplex_account.ping() - return self._myplex_account - - return await asyncio.to_thread(_refresh_plex_token) diff --git a/music_assistant/server/providers/plex/helpers.py b/music_assistant/server/providers/plex/helpers.py deleted file mode 100644 index af33e53d..00000000 --- a/music_assistant/server/providers/plex/helpers.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Several helpers/utils for the Plex Music Provider.""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING, cast - -import requests -from plexapi.gdm import GDM -from plexapi.library import LibrarySection as PlexLibrarySection -from plexapi.library import MusicSection as PlexMusicSection -from plexapi.server import PlexServer - -if TYPE_CHECKING: - from music_assistant.server import MusicAssistant - - -async def get_libraries( - mass: MusicAssistant, - auth_token: str | None, - local_server_ssl: bool, - local_server_ip: str, - local_server_port: str, - local_server_verify_cert: bool, -) -> list[str]: - """ - Get all music libraries for all plex servers. - - Returns a dict of Library names in format {'servername / library name':'baseurl'} - """ - cache_key = "plex_libraries" - - def _get_libraries() -> list[str]: - # create a listing of available music libraries on all servers - all_libraries: list[str] = [] - session = requests.Session() - session.verify = local_server_verify_cert - local_server_protocol = "https" if local_server_ssl else "http" - plex_server: PlexServer - if auth_token is None: - plex_server = PlexServer( - f"{local_server_protocol}://{local_server_ip}:{local_server_port}" - ) - else: - plex_server = PlexServer( - f"{local_server_protocol}://{local_server_ip}:{local_server_port}", - auth_token, - session=session, - ) - for media_section in cast(list[PlexLibrarySection], plex_server.library.sections()): - if media_section.type != PlexMusicSection.TYPE: - continue - # TODO: figure out what plex uses as stable id and use that instead of names - all_libraries.append(f"{plex_server.friendlyName} / {media_section.title}") - return all_libraries - - if cache := await mass.cache.get(cache_key, checksum=auth_token): - return cast(list[str], cache) - - result = await asyncio.to_thread(_get_libraries) - # use short expiration for in-memory cache - await mass.cache.set(cache_key, result, checksum=auth_token, expiration=3600) - return result - - -async def discover_local_servers() -> tuple[str, int] | tuple[None, None]: - """Discover all local plex servers on the network.""" - - def _discover_local_servers() -> tuple[str, int] | tuple[None, None]: - gdm = GDM() - gdm.scan() - if len(gdm.entries) > 0: - entry = gdm.entries[0] - data = entry.get("data") - local_server_ip = entry.get("from")[0] - local_server_port = data.get("Port") - return local_server_ip, local_server_port - else: - return None, None - - return await asyncio.to_thread(_discover_local_servers) diff --git a/music_assistant/server/providers/plex/icon.svg b/music_assistant/server/providers/plex/icon.svg deleted file mode 100644 index 7c994b84..00000000 --- a/music_assistant/server/providers/plex/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/music_assistant/server/providers/plex/manifest.json b/music_assistant/server/providers/plex/manifest.json deleted file mode 100644 index 9ced2037..00000000 --- a/music_assistant/server/providers/plex/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "music", - "domain": "plex", - "name": "Plex Media Server Library", - "description": "Support for the Plex streaming provider in Music Assistant.", - "codeowners": [ - "@micha91" - ], - "requirements": [ - "plexapi==4.15.16" - ], - "documentation": "https://music-assistant.io/music-providers/plex/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py deleted file mode 100644 index 33ebeb4f..00000000 --- a/music_assistant/server/providers/qobuz/__init__.py +++ /dev/null @@ -1,829 +0,0 @@ -"""Qobuz musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -import datetime -import hashlib -import time -from contextlib import suppress -from typing import TYPE_CHECKING - -from aiohttp import client_exceptions - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version, try_parse_int -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - InvalidDataError, - LoginFailed, - MediaNotFoundError, - ResourceTemporarilyUnavailable, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - AudioFormat, - ContentType, - ImageType, - MediaItemImage, - MediaItemType, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import ( - CONF_PASSWORD, - CONF_USERNAME, - VARIOUS_ARTISTS_MBID, - VARIOUS_ARTISTS_NAME, -) -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.helpers.util import lock -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, -) - -VARIOUS_ARTISTS_ID = "145383" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return QobuzProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - ), - ConfigEntry( - key=CONF_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password", - required=True, - ), - ) - - -class QobuzProvider(MusicProvider): - """Provider for the Qobux music service.""" - - _user_auth_info: str | None = None - # rate limiter needs to be specified on provider-level, - # so make it an instance attribute - throttler = ThrottlerManager(rate_limit=1, period=2) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - if not self.config.get_value(CONF_USERNAME) or not self.config.get_value(CONF_PASSWORD): - msg = "Invalid login credentials" - raise LoginFailed(msg) - # try to get a token, raise if that fails - token = await self._auth_token() - if not token: - msg = f"Login failed for user {self.config.get_value(CONF_USERNAME)}" - raise LoginFailed(msg) - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def search( - self, search_query: str, media_types=list[MediaType], limit: int = 5 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - result = SearchResults() - media_types = [ - x - for x in media_types - if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) - ] - if not media_types: - return result - params = {"query": search_query, "limit": limit} - if len(media_types) == 1: - # qobuz does not support multiple searchtypes, falls back to all if no type given - if media_types[0] == MediaType.ARTIST: - params["type"] = "artists" - if media_types[0] == MediaType.ALBUM: - params["type"] = "albums" - if media_types[0] == MediaType.TRACK: - params["type"] = "tracks" - if media_types[0] == MediaType.PLAYLIST: - params["type"] = "playlists" - if searchresult := await self._get_data("catalog/search", **params): - if "artists" in searchresult and MediaType.ARTIST in media_types: - result.artists += [ - self._parse_artist(item) - for item in searchresult["artists"]["items"] - if (item and item["id"]) - ] - if "albums" in searchresult and MediaType.ALBUM in media_types: - result.albums += [ - await self._parse_album(item) - for item in searchresult["albums"]["items"] - if (item and item["id"]) - ] - if "tracks" in searchresult and MediaType.TRACK in media_types: - result.tracks += [ - await self._parse_track(item) - for item in searchresult["tracks"]["items"] - if (item and item["id"]) - ] - if "playlists" in searchresult and MediaType.PLAYLIST in media_types: - result.playlists += [ - self._parse_playlist(item) - for item in searchresult["playlists"]["items"] - if (item and item["id"]) - ] - return result - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Qobuz.""" - endpoint = "favorite/getUserFavorites" - for item in await self._get_all_items(endpoint, key="artists", type="artists"): - if item and item["id"]: - yield self._parse_artist(item) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Qobuz.""" - endpoint = "favorite/getUserFavorites" - for item in await self._get_all_items(endpoint, key="albums", type="albums"): - if item and item["id"]: - yield await self._parse_album(item) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Qobuz.""" - endpoint = "favorite/getUserFavorites" - for item in await self._get_all_items(endpoint, key="tracks", type="tracks"): - if item and item["id"]: - yield await self._parse_track(item) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from the provider.""" - endpoint = "playlist/getUserPlaylists" - for item in await self._get_all_items(endpoint, key="playlists"): - if item and item["id"]: - yield self._parse_playlist(item) - - async def get_artist(self, prov_artist_id) -> Artist: - """Get full artist details by id.""" - params = {"artist_id": prov_artist_id} - if (artist_obj := await self._get_data("artist/get", **params)) and artist_obj["id"]: - return self._parse_artist(artist_obj) - msg = f"Item {prov_artist_id} not found" - raise MediaNotFoundError(msg) - - async def get_album(self, prov_album_id) -> Album: - """Get full album details by id.""" - params = {"album_id": prov_album_id} - if (album_obj := await self._get_data("album/get", **params)) and album_obj["id"]: - return await self._parse_album(album_obj) - msg = f"Item {prov_album_id} not found" - raise MediaNotFoundError(msg) - - async def get_track(self, prov_track_id) -> Track: - """Get full track details by id.""" - params = {"track_id": prov_track_id} - if (track_obj := await self._get_data("track/get", **params)) and track_obj["id"]: - return await self._parse_track(track_obj) - msg = f"Item {prov_track_id} not found" - raise MediaNotFoundError(msg) - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Get full playlist details by id.""" - params = {"playlist_id": prov_playlist_id} - if (playlist_obj := await self._get_data("playlist/get", **params)) and playlist_obj["id"]: - return self._parse_playlist(playlist_obj) - msg = f"Item {prov_playlist_id} not found" - raise MediaNotFoundError(msg) - - async def get_album_tracks(self, prov_album_id) -> list[Track]: - """Get all album tracks for given album id.""" - params = {"album_id": prov_album_id} - return [ - await self._parse_track(item) - for item in await self._get_all_items("album/get", **params, key="tracks") - if (item and item["id"]) - ] - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - page_size = 100 - offset = page * page_size - qobuz_result = await self._get_data( - "playlist/get", - key="tracks", - playlist_id=prov_playlist_id, - extra="tracks", - offset=offset, - limit=page_size, - ) - for index, track_obj in enumerate(qobuz_result["tracks"]["items"], 1): - if not (track_obj and track_obj["id"]): - continue - track = await self._parse_track(track_obj) - track.position = index + offset - result.append(track) - return result - - async def get_artist_albums(self, prov_artist_id) -> list[Album]: - """Get a list of albums for the given artist.""" - result = await self._get_data( - "artist/get", - artist_id=prov_artist_id, - extra="albums", - offset=0, - limit=100, - ) - return [ - await self._parse_album(item) - for item in result["albums"]["items"] - if (item and item["id"] and str(item["artist"]["id"]) == prov_artist_id) - ] - - async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: - """Get a list of most popular tracks for the given artist.""" - result = await self._get_data( - "artist/get", - artist_id=prov_artist_id, - extra="playlists", - offset=0, - limit=25, - ) - if result and result["playlists"]: - return [ - await self._parse_track(item) - for item in result["playlists"][0]["tracks"]["items"] - if (item and item["id"]) - ] - # fallback to search - artist = await self.get_artist(prov_artist_id) - searchresult = await self._get_data( - "catalog/search", query=artist.name, limit=25, type="tracks" - ) - return [ - await self._parse_track(item) - for item in searchresult["tracks"]["items"] - if ( - item - and item["id"] - and "performer" in item - and str(item["performer"]["id"]) == str(prov_artist_id) - ) - ] - - async def get_similar_artists(self, prov_artist_id) -> None: - """Get similar artists for given artist.""" - # https://www.qobuz.com/api.json/0.2/artist/getSimilarArtists?artist_id=220020&offset=0&limit=3 - - async def library_add(self, item: MediaItemType): - """Add item to library.""" - result = None - if item.media_type == MediaType.ARTIST: - result = await self._get_data("favorite/create", artist_id=item.item_id) - elif item.media_type == MediaType.ALBUM: - result = await self._get_data("favorite/create", album_ids=item.item_id) - elif item.media_type == MediaType.TRACK: - result = await self._get_data("favorite/create", track_ids=item.item_id) - elif item.media_type == MediaType.PLAYLIST: - result = await self._get_data("playlist/subscribe", playlist_id=item.item_id) - return result - - async def library_remove(self, prov_item_id, media_type: MediaType): - """Remove item from library.""" - result = None - if media_type == MediaType.ARTIST: - result = await self._get_data("favorite/delete", artist_ids=prov_item_id) - elif media_type == MediaType.ALBUM: - result = await self._get_data("favorite/delete", album_ids=prov_item_id) - elif media_type == MediaType.TRACK: - result = await self._get_data("favorite/delete", track_ids=prov_item_id) - elif media_type == MediaType.PLAYLIST: - playlist = await self.get_playlist(prov_item_id) - if playlist.is_editable: - result = await self._get_data("playlist/delete", playlist_id=prov_item_id) - else: - result = await self._get_data("playlist/unsubscribe", playlist_id=prov_item_id) - return result - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - return await self._get_data( - "playlist/addTracks", - playlist_id=prov_playlist_id, - track_ids=",".join(prov_track_ids), - playlist_track_ids=",".join(prov_track_ids), - ) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int] - ) -> None: - """Remove track(s) from playlist.""" - playlist_track_ids = set() - for pos in positions_to_remove: - idx = pos - 1 - qobuz_result = await self._get_data( - "playlist/get", - key="tracks", - playlist_id=prov_playlist_id, - extra="tracks", - offset=idx, - limit=1, - ) - if not qobuz_result: - continue - playlist_track_id = qobuz_result["tracks"]["items"][0]["playlist_track_id"] - playlist_track_ids.add(str(playlist_track_id)) - - return await self._get_data( - "playlist/deleteTracks", - playlist_id=prov_playlist_id, - playlist_track_ids=",".join(playlist_track_ids), - ) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - streamdata = None - for format_id in [27, 7, 6, 5]: - # it seems that simply requesting for highest available quality does not work - # from time to time the api response is empty for this request ?! - result = await self._get_data( - "track/getFileUrl", - sign_request=True, - format_id=format_id, - track_id=item_id, - intent="stream", - ) - if result and result.get("url"): - streamdata = result - break - if not streamdata: - msg = f"Unable to retrieve stream details for {item_id}" - raise MediaNotFoundError(msg) - if streamdata["mime_type"] == "audio/mpeg": - content_type = ContentType.MPEG - elif streamdata["mime_type"] == "audio/flac": - content_type = ContentType.FLAC - else: - msg = f"Unsupported mime type for {item_id}" - raise MediaNotFoundError(msg) - self.mass.create_task(self._report_playback_started(streamdata)) - return StreamDetails( - item_id=str(item_id), - provider=self.instance_id, - audio_format=AudioFormat( - content_type=content_type, - sample_rate=int(streamdata["sampling_rate"] * 1000), - bit_depth=streamdata["bit_depth"], - ), - stream_type=StreamType.HTTP, - duration=streamdata["duration"], - data=streamdata, # we need these details for reporting playback - path=streamdata["url"], - ) - - async def _report_playback_started(self, streamdata: dict) -> None: - """Report playback start to qobuz.""" - # TODO: need to figure out if the streamed track is purchased by user - # https://www.qobuz.com/api.json/0.2/purchase/getUserPurchasesIds?limit=5000&user_id=xxxxxxx - # {"albums":{"total":0,"items":[]}, - # "tracks":{"total":0,"items":[]},"user":{"id":xxxx,"login":"xxxxx"}} - device_id = self._user_auth_info["user"]["device"]["id"] - credential_id = self._user_auth_info["user"]["credential"]["id"] - user_id = self._user_auth_info["user"]["id"] - format_id = streamdata["format_id"] - timestamp = int(time.time()) - events = [ - { - "online": True, - "sample": False, - "intent": "stream", - "device_id": device_id, - "track_id": streamdata["track_id"], - "purchase": False, - "date": timestamp, - "credential_id": credential_id, - "user_id": user_id, - "local": False, - "format_id": format_id, - } - ] - async with self.throttler.bypass(): - await self._post_data("track/reportStreamingStart", data=events) - - async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: - """Handle callback when an item completed streaming.""" - user_id = self._user_auth_info["user"]["id"] - async with self.throttler.bypass(): - await self._get_data( - "/track/reportStreamingEnd", - user_id=user_id, - track_id=str(streamdetails.item_id), - duration=try_parse_int(seconds_streamed), - ) - - def _parse_artist(self, artist_obj: dict): - """Parse qobuz artist object to generic layout.""" - artist = Artist( - item_id=str(artist_obj["id"]), - provider=self.domain, - name=artist_obj["name"], - provider_mappings={ - ProviderMapping( - item_id=str(artist_obj["id"]), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f'https://open.qobuz.com/artist/{artist_obj["id"]}', - ) - }, - ) - if artist.item_id == VARIOUS_ARTISTS_ID: - artist.mbid = VARIOUS_ARTISTS_MBID - artist.name = VARIOUS_ARTISTS_NAME - if img := self.__get_image(artist_obj): - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if artist_obj.get("biography"): - artist.metadata.description = artist_obj["biography"].get("content") - return artist - - async def _parse_album(self, album_obj: dict, artist_obj: dict | None = None): - """Parse qobuz album object to generic layout.""" - if not artist_obj and "artist" not in album_obj: - # artist missing in album info, return full abum instead - return await self.get_album(album_obj["id"]) - name, version = parse_title_and_version(album_obj["title"], album_obj.get("version")) - album = Album( - item_id=str(album_obj["id"]), - provider=self.domain, - name=name, - version=version, - provider_mappings={ - ProviderMapping( - item_id=str(album_obj["id"]), - provider_domain=self.domain, - provider_instance=self.instance_id, - available=album_obj["streamable"] and album_obj["displayable"], - audio_format=AudioFormat( - content_type=ContentType.FLAC, - sample_rate=album_obj["maximum_sampling_rate"] * 1000, - bit_depth=album_obj["maximum_bit_depth"], - ), - url=f'https://open.qobuz.com/album/{album_obj["id"]}', - ) - }, - ) - album.external_ids.add((ExternalID.BARCODE, album_obj["upc"])) - album.artists.append(self._parse_artist(artist_obj or album_obj["artist"])) - if ( - album_obj.get("product_type", "") == "single" - or album_obj.get("release_type", "") == "single" - ): - album.album_type = AlbumType.SINGLE - elif ( - album_obj.get("product_type", "") == "compilation" or "Various" in album.artists[0].name - ): - album.album_type = AlbumType.COMPILATION - elif ( - album_obj.get("product_type", "") == "album" - or album_obj.get("release_type", "") == "album" - ): - album.album_type = AlbumType.ALBUM - if "genre" in album_obj: - album.metadata.genres = {album_obj["genre"]["name"]} - if img := self.__get_image(album_obj): - album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if "label" in album_obj: - album.metadata.label = album_obj["label"]["name"] - if released_at := album_obj.get("released_at"): - with suppress(ValueError): - album.year = datetime.datetime.fromtimestamp(released_at).year - if album_obj.get("copyright"): - album.metadata.copyright = album_obj["copyright"] - if album_obj.get("description"): - album.metadata.description = album_obj["description"] - if album_obj.get("parental_warning"): - album.metadata.explicit = True - return album - - async def _parse_track(self, track_obj: dict) -> Track: - """Parse qobuz track object to generic layout.""" - name, version = parse_title_and_version(track_obj["title"], track_obj.get("version")) - track = Track( - item_id=str(track_obj["id"]), - provider=self.domain, - name=name, - version=version, - duration=track_obj["duration"], - provider_mappings={ - ProviderMapping( - item_id=str(track_obj["id"]), - provider_domain=self.domain, - provider_instance=self.instance_id, - available=track_obj["streamable"] and track_obj["displayable"], - audio_format=AudioFormat( - content_type=ContentType.FLAC, - sample_rate=track_obj["maximum_sampling_rate"] * 1000, - bit_depth=track_obj["maximum_bit_depth"], - ), - url=f'https://open.qobuz.com/track/{track_obj["id"]}', - ) - }, - disc_number=track_obj.get("media_number", 0), - track_number=track_obj.get("track_number", 0), - ) - if isrc := track_obj.get("isrc"): - track.external_ids.add((ExternalID.ISRC, isrc)) - if track_obj.get("performer") and "Various " not in track_obj["performer"]: - artist = self._parse_artist(track_obj["performer"]) - if artist: - track.artists.append(artist) - # try to grab artist from album - if not track.artists and ( - track_obj.get("album") - and track_obj["album"].get("artist") - and "Various " not in track_obj["album"]["artist"] - ): - artist = self._parse_artist(track_obj["album"]["artist"]) - if artist: - track.artists.append(artist) - if not track.artists: - # last resort: parse from performers string - for performer_str in track_obj["performers"].split(" - "): - role = performer_str.split(", ")[1] - name = performer_str.split(", ")[0] - if "artist" in role.lower(): - artist = Artist( - item_id=name, - provider=self.domain, - name=name, - provider_mappings={ - ProviderMapping( - item_id=name, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - track.artists.append(artist) - # TODO: fix grabbing composer from details - - if "album" in track_obj: - album = await self._parse_album(track_obj["album"]) - if album: - track.album = album - if track_obj.get("performers"): - track.metadata.performers = {x.strip() for x in track_obj["performers"].split("-")} - if track_obj.get("copyright"): - track.metadata.copyright = track_obj["copyright"] - if track_obj.get("parental_warning"): - track.metadata.explicit = True - if img := self.__get_image(track_obj): - track.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - - return track - - def _parse_playlist(self, playlist_obj): - """Parse qobuz playlist object to generic layout.""" - playlist = Playlist( - item_id=str(playlist_obj["id"]), - provider=self.domain, - name=playlist_obj["name"], - owner=playlist_obj["owner"]["name"], - provider_mappings={ - ProviderMapping( - item_id=str(playlist_obj["id"]), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f'https://open.qobuz.com/playlist/{playlist_obj["id"]}', - ) - }, - ) - playlist.is_editable = ( - playlist_obj["owner"]["id"] == self._user_auth_info["user"]["id"] - or playlist_obj["is_collaborative"] - ) - if img := self.__get_image(playlist_obj): - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - playlist.cache_checksum = str(playlist_obj["updated_at"]) - return playlist - - @lock - async def _auth_token(self): - """Login to qobuz and store the token.""" - if self._user_auth_info: - return self._user_auth_info["user_auth_token"] - params = { - "username": self.config.get_value(CONF_USERNAME), - "password": self.config.get_value(CONF_PASSWORD), - "device_manufacturer_id": "music_assistant", - } - details = await self._get_data("user/login", **params) - if details and "user" in details: - self._user_auth_info = details - self.logger.info( - "Successfully logged in to Qobuz as %s", details["user"]["display_name"] - ) - self.mass.metadata.set_default_preferred_language(details["user"]["country_code"]) - return details["user_auth_token"] - return None - - async def _get_all_items(self, endpoint, key="tracks", **kwargs): - """Get all items from a paged list.""" - limit = 50 - offset = 0 - all_items = [] - while True: - kwargs["limit"] = limit - kwargs["offset"] = offset - result = await self._get_data(endpoint, **kwargs) - offset += limit - if not result: - break - if not result.get(key) or not result[key].get("items"): - break - for item in result[key]["items"]: - all_items.append(item) - if len(result[key]["items"]) < limit: - break - return all_items - - @throttle_with_retries - async def _get_data(self, endpoint, sign_request=False, **kwargs): - """Get data from api.""" - self.logger.debug("Handling GET request to %s", endpoint) - url = f"http://www.qobuz.com/api.json/0.2/{endpoint}" - headers = {"X-App-Id": app_var(0)} - locale = self.mass.metadata.locale.replace("_", "-") - language = locale.split("-")[0] - headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" - if endpoint != "user/login": - auth_token = await self._auth_token() - if not auth_token: - self.logger.debug("Not logged in") - return None - headers["X-User-Auth-Token"] = auth_token - if sign_request: - signing_data = "".join(endpoint.split("/")) - keys = list(kwargs.keys()) - keys.sort() - for key in keys: - signing_data += f"{key}{kwargs[key]}" - request_ts = str(time.time()) - request_sig = signing_data + request_ts + app_var(1) - request_sig = str(hashlib.md5(request_sig.encode()).hexdigest()) - kwargs["request_ts"] = request_ts - kwargs["request_sig"] = request_sig - kwargs["app_id"] = app_var(0) - kwargs["user_auth_token"] = await self._auth_token() - async with ( - self.mass.http_session.get(url, headers=headers, params=kwargs) as response, - ): - # handle rate limiter - if response.status == 429: - backoff_time = int(response.headers.get("Retry-After", 0)) - raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - # handle 404 not found, convert to MediaNotFoundError - if response.status == 404: - raise MediaNotFoundError(f"{endpoint} not found") - response.raise_for_status() - try: - return await response.json(loads=json_loads) - except client_exceptions.ContentTypeError as err: - text = err.message or await response.text() or err.status - msg = f"Error while handling {endpoint}: {text}" - raise InvalidDataError(msg) - - @throttle_with_retries - async def _post_data(self, endpoint, params=None, data=None): - """Post data to api.""" - self.logger.debug("Handling POST request to %s", endpoint) - if not params: - params = {} - if not data: - data = {} - url = f"http://www.qobuz.com/api.json/0.2/{endpoint}" - params["app_id"] = app_var(0) - params["user_auth_token"] = await self._auth_token() - async with self.mass.http_session.post( - url, params=params, json=data, ssl=False - ) as response: - # handle rate limiter - if response.status == 429: - backoff_time = int(response.headers.get("Retry-After", 0)) - raise ResourceTemporarilyUnavailable("Rate Limiter", backoff_time=backoff_time) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - # handle 404 not found, convert to MediaNotFoundError - if response.status == 404: - raise MediaNotFoundError(f"{endpoint} not found") - response.raise_for_status() - return await response.json(loads=json_loads) - - def __get_image(self, obj: dict) -> str | None: - """Try to parse image from Qobuz media object.""" - if obj.get("image"): - for key in ["extralarge", "large", "medium", "small"]: - if obj["image"].get(key): - if "2a96cbd8b46e442fc41c2b86b821562f" in obj["image"][key]: - continue - return obj["image"][key] - if obj.get("images300"): - # playlists seem to use this strange format - return obj["images300"][0] - if obj.get("album"): - return self.__get_image(obj["album"]) - if obj.get("artist"): - return self.__get_image(obj["artist"]) - return None diff --git a/music_assistant/server/providers/qobuz/icon.svg b/music_assistant/server/providers/qobuz/icon.svg deleted file mode 100644 index 38d3349f..00000000 --- a/music_assistant/server/providers/qobuz/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/music_assistant/server/providers/qobuz/icon_dark.svg b/music_assistant/server/providers/qobuz/icon_dark.svg deleted file mode 100644 index 2fc68c88..00000000 --- a/music_assistant/server/providers/qobuz/icon_dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/music_assistant/server/providers/qobuz/manifest.json b/music_assistant/server/providers/qobuz/manifest.json deleted file mode 100644 index 61f77022..00000000 --- a/music_assistant/server/providers/qobuz/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "qobuz", - "name": "Qobuz", - "description": "Qobuz support for Music Assistant: Lossless (and hi-res) Music provider.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/music-providers/qobuz/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/server/providers/radiobrowser/__init__.py deleted file mode 100644 index 5675f4c6..00000000 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ /dev/null @@ -1,366 +0,0 @@ -"""RadioBrowser musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator, Sequence -from typing import TYPE_CHECKING, cast - -from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( - ConfigEntryType, - LinkType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import MediaNotFoundError -from music_assistant.common.models.media_items import ( - AudioFormat, - BrowseFolder, - ContentType, - ImageType, - MediaItemImage, - MediaItemLink, - MediaItemType, - MediaType, - ProviderMapping, - Radio, - SearchResults, - UniqueList, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.models.music_provider import MusicProvider - -SUPPORTED_FEATURES = ( - ProviderFeature.SEARCH, - ProviderFeature.BROWSE, - # RadioBrowser doesn't support a library feature at all - # but MA users like to favorite their radio stations and - # have that included in backups so we store it in the config. - ProviderFeature.LIBRARY_RADIOS, - ProviderFeature.LIBRARY_RADIOS_EDIT, -) - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -CONF_STORED_RADIOS = "stored_radios" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return RadioBrowserProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 D205 - return ( - ConfigEntry( - # RadioBrowser doesn't support a library feature at all - # but MA users like to favorite their radio stations and - # have that included in backups so we store it in the config. - key=CONF_STORED_RADIOS, - type=ConfigEntryType.STRING, - label=CONF_STORED_RADIOS, - default_value=[], - required=False, - multi_value=True, - hidden=True, - ), - ) - - -class RadioBrowserProvider(MusicProvider): - """Provider implementation for RadioBrowser.""" - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.radios = RadioBrowser( - session=self.mass.http_session, user_agent=f"MusicAssistant/{self.mass.version}" - ) - try: - # Try to get some stats to check connection to RadioBrowser API - await self.radios.stats() - except RadioBrowserError as err: - self.logger.exception("%s", err) - - # copy the radiobrowser items that were added to the library - # TODO: remove this logic after version 2.3.0 or later - if not self.config.get_value(CONF_STORED_RADIOS) and self.mass.music.database: - async for db_row in self.mass.music.database.iter_items( - "provider_mappings", - {"media_type": "radio", "provider_domain": "radiobrowser"}, - ): - await self.library_add(await self.get_radio(db_row["provider_item_id"])) - - async def search( - self, search_query: str, media_types: list[MediaType], limit: int = 10 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - result = SearchResults() - if MediaType.RADIO not in media_types: - return result - - searchresult = await self.radios.search(name=search_query, limit=limit) - result.radio = [await self._parse_radio(item) for item in searchresult] - - return result - - async def browse(self, path: str) -> Sequence[MediaItemType]: - """Browse this provider's items. - - :param path: The path to browse, (e.g. provid://artists). - """ - part_parts = path.split("://")[1].split("/") - subpath = part_parts[0] if part_parts else "" - subsubpath = part_parts[1] if len(part_parts) > 1 else "" - - if not subpath: - # return main listing - return [ - BrowseFolder( - item_id="popular", - provider=self.domain, - path=path + "popular", - name="", - label="radiobrowser_by_popularity", - ), - BrowseFolder( - item_id="country", - provider=self.domain, - path=path + "country", - name="", - label="radiobrowser_by_country", - ), - BrowseFolder( - item_id="tag", - provider=self.domain, - path=path + "tag", - name="", - label="radiobrowser_by_tag", - ), - ] - - if subpath == "popular": - return await self.get_by_popularity() - - if subpath == "tag" and subsubpath: - return await self.get_by_tag(subsubpath) - - if subpath == "tag": - return await self.get_tag_folders(path) - - if subpath == "country" and subsubpath: - return await self.get_by_country(subsubpath) - - if subpath == "country": - return await self.get_country_folders(path) - - return [] - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - stored_radios = self.config.get_value(CONF_STORED_RADIOS) - if TYPE_CHECKING: - stored_radios = cast(list[str], stored_radios) - for item in stored_radios: - yield await self.get_radio(item) - - async def library_add(self, item: MediaItemType) -> bool: - """Add item to provider's library. Return true on success.""" - stored_radios = self.config.get_value(CONF_STORED_RADIOS) - if TYPE_CHECKING: - stored_radios = cast(list[str], stored_radios) - if item.item_id in stored_radios: - return False - self.logger.debug("Adding radio %s to stored radios", item.item_id) - stored_radios = [*stored_radios, item.item_id] - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_STORED_RADIOS, stored_radios - ) - return True - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove item from provider's library. Return true on success.""" - stored_radios = self.config.get_value(CONF_STORED_RADIOS) - if TYPE_CHECKING: - stored_radios = cast(list[str], stored_radios) - if prov_item_id not in stored_radios: - return False - self.logger.debug("Removing radio %s from stored radios", prov_item_id) - stored_radios = [x for x in stored_radios if x != prov_item_id] - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_STORED_RADIOS, stored_radios - ) - return True - - @use_cache(3600 * 24) - async def get_tag_folders(self, base_path: str) -> list[BrowseFolder]: - """Get a list of tag names as BrowseFolder.""" - tags = await self.radios.tags( - hide_broken=True, - order=Order.STATION_COUNT, - reverse=True, - ) - tags.sort(key=lambda tag: tag.name) - return [ - BrowseFolder( - item_id=tag.name.lower(), - provider=self.domain, - path=base_path + "/" + tag.name.lower(), - name=tag.name, - ) - for tag in tags - ] - - @use_cache(3600 * 24) - async def get_country_folders(self, base_path: str) -> list[BrowseFolder]: - """Get a list of country names as BrowseFolder.""" - items: list[BrowseFolder] = [] - for country in await self.radios.countries(order=Order.NAME, hide_broken=True, limit=1000): - folder = BrowseFolder( - item_id=country.code.lower(), - provider=self.domain, - path=base_path + "/" + country.code.lower(), - name=country.name, - ) - folder.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=country.favicon, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - items.append(folder) - return items - - @use_cache(3600) - async def get_by_popularity(self) -> Sequence[Radio]: - """Get radio stations by popularity.""" - stations = await self.radios.stations( - hide_broken=True, - limit=1000, - order=Order.CLICK_COUNT, - reverse=True, - ) - items = [] - for station in stations: - items.append(await self._parse_radio(station)) - return items - - @use_cache(3600) - async def get_by_tag(self, tag: str) -> Sequence[Radio]: - """Get radio stations by tag.""" - items = [] - stations = await self.radios.stations( - filter_by=FilterBy.TAG_EXACT, - filter_term=tag, - hide_broken=True, - limit=1000, - order=Order.CLICK_COUNT, - reverse=False, - ) - for station in stations: - items.append(await self._parse_radio(station)) - return items - - @use_cache(3600) - async def get_by_country(self, country_code: str) -> list[Radio]: - """Get radio stations by country.""" - items = [] - stations = await self.radios.stations( - filter_by=FilterBy.COUNTRY_CODE_EXACT, - filter_term=country_code, - hide_broken=True, - limit=1000, - order=Order.CLICK_COUNT, - reverse=False, - ) - for station in stations: - items.append(await self._parse_radio(station)) - return items - - async def get_radio(self, prov_radio_id: str) -> Radio: - """Get radio station details.""" - radio = await self.radios.station(uuid=prov_radio_id) - if not radio: - raise MediaNotFoundError(f"Radio station {prov_radio_id} not found") - return await self._parse_radio(radio) - - async def _parse_radio(self, radio_obj: Station) -> Radio: - """Parse Radio object from json obj returned from api.""" - radio = Radio( - item_id=radio_obj.uuid, - provider=self.domain, - name=radio_obj.name, - provider_mappings={ - ProviderMapping( - item_id=radio_obj.uuid, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - radio.metadata.popularity = radio_obj.votes - radio.metadata.links = {MediaItemLink(type=LinkType.WEBSITE, url=radio_obj.homepage)} - radio.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=radio_obj.favicon, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - - return radio - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a radio station.""" - stream = await self.radios.station(uuid=item_id) - if not stream: - raise MediaNotFoundError(f"Radio station {item_id} not found") - await self.radios.station_click(uuid=item_id) - return StreamDetails( - provider=self.domain, - item_id=item_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(stream.codec), - ), - media_type=MediaType.RADIO, - stream_type=StreamType.HTTP, - path=stream.url_resolved, - can_seek=False, - ) diff --git a/music_assistant/server/providers/radiobrowser/manifest.json b/music_assistant/server/providers/radiobrowser/manifest.json deleted file mode 100644 index 263a29d5..00000000 --- a/music_assistant/server/providers/radiobrowser/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "music", - "domain": "radiobrowser", - "name": "RadioBrowser", - "description": "Search radio streams from RadioBrowser in Music Assistant.", - "codeowners": ["@gieljnssns"], - "requirements": ["radios==0.3.2"], - "documentation": "https://music-assistant.io/music-providers/radio-browser/", - "multi_instance": false, - "icon": "radio" -} diff --git a/music_assistant/server/providers/siriusxm/__init__.py b/music_assistant/server/providers/siriusxm/__init__.py deleted file mode 100644 index b4c04f00..00000000 --- a/music_assistant/server/providers/siriusxm/__init__.py +++ /dev/null @@ -1,329 +0,0 @@ -"""SiriusXM Music Provider for Music Assistant.""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator, Awaitable, Sequence -from typing import TYPE_CHECKING, Any, cast - -from music_assistant.common.helpers.util import select_free_port -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - LinkType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( - AudioFormat, - ImageType, - ItemMapping, - MediaItemImage, - MediaItemLink, - MediaItemType, - ProviderMapping, - Radio, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -import sxm.http -from sxm import SXMClientAsync -from sxm.models import QualitySize, RegionChoice, XMChannel, XMLiveChannel - -CONF_SXM_USERNAME = "sxm_email_address" -CONF_SXM_PASSWORD = "sxm_password" -CONF_SXM_REGION = "sxm_region" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return SiriusXMProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_SXM_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - ), - ConfigEntry( - key=CONF_SXM_PASSWORD, - type=ConfigEntryType.SECURE_STRING, - label="Password", - required=True, - ), - ConfigEntry( - key=CONF_SXM_REGION, - type=ConfigEntryType.STRING, - default_value="US", - options=( - ConfigValueOption(title="United States", value="US"), - ConfigValueOption(title="Canada", value="CA"), - ), - label="Region", - required=True, - ), - ) - - -class SiriusXMProvider(MusicProvider): - """SiriusXM Music Provider.""" - - _username: str - _password: str - _region: str - _client: SXMClientAsync - - _channels: list[XMChannel] - - _sxm_server: Webserver - _base_url: str - - _current_stream_details: StreamDetails | None = None - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return ( - ProviderFeature.BROWSE, - ProviderFeature.LIBRARY_RADIOS, - ) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - username: str = self.config.get_value(CONF_SXM_USERNAME) - password: str = self.config.get_value(CONF_SXM_PASSWORD) - - region: RegionChoice = ( - RegionChoice.US if self.config.get_value(CONF_SXM_REGION) == "US" else RegionChoice.CA - ) - - self._client = SXMClientAsync( - username, - password, - region, - quality=QualitySize.LARGE_256k, - update_handler=self._channel_updated, - ) - - self.logger.info("Authenticating with SiriusXM") - if not await self._client.authenticate(): - raise LoginFailed("Could not login to SiriusXM") - - self.logger.info("Successfully authenticated") - - await self._refresh_channels() - - # Set up the sxm server for streaming - bind_ip = "127.0.0.1" - bind_port = await select_free_port(8100, 9999) - - self._base_url = f"{bind_ip}:{bind_port}" - http_handler = sxm.http.make_http_handler(self._client) - - self._sxm_server = Webserver(self.logger) - - await self._sxm_server.setup( - bind_ip=bind_ip, - bind_port=bind_port, - base_url=self._base_url, - static_routes=[ - ("*", "/{tail:.*}", cast(Awaitable, http_handler)), - ], - ) - - self.logger.debug(f"SXM Proxy server running at {bind_ip}:{bind_port}") - - async def unload(self) -> None: - """ - Handle unload/close of the provider. - - Called when provider is deregistered (e.g. MA exiting or config reloading). - """ - await self._sxm_server.close() - - @property - def is_streaming_provider(self) -> bool: - """ - Return True if the provider is a streaming provider. - - This literally means that the catalog is not the same as the library contents. - For local based providers (files, plex), the catalog is the same as the library content. - It also means that data is if this provider is NOT a streaming provider, - data cross instances is unique, the catalog and library differs per instance. - - Setting this to True will only query one instance of the provider for search and lookups. - Setting this to False will query all instances of this provider for search and lookups. - """ - return True - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - for channel in self._channels_by_id.values(): - if channel.is_favorite: - yield self._parse_radio(channel) - - async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[return] - """Get full radio details by id.""" - if prov_radio_id not in self._channels_by_id: - raise MediaNotFoundError("Station not found") - - return self._parse_radio(self._channels_by_id[prov_radio_id]) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track/radio.""" - hls_path = f"http://{self._base_url}/{item_id}.m3u8" - - # Keep a reference to the current `StreamDetails` object so that we can - # update the `stream_title` attribute as callbacks come in from the - # sxm-client with the channel's live data. - # See `_channel_updated` for where this is handled. - self._current_stream_details = StreamDetails( - item_id=item_id, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.AAC, - ), - stream_type=StreamType.HLS, - media_type=MediaType.RADIO, - path=hls_path, - can_seek=False, - ) - - return self._current_stream_details - - async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]: - """Browse this provider's items. - - :param path: The path to browse, (e.g. provider_id://artists). - """ - return [self._parse_radio(channel) for channel in self._channels] - - def _channel_updated(self, live_channel_raw: dict[str, Any]) -> None: - """Handle a channel update event.""" - live_data = XMLiveChannel.from_dict(live_channel_raw) - - self.logger.debug(f"Got update for SiriusXM channel {live_data.id}") - current_channel = self._current_stream_details.item_id - - if live_data.id != current_channel: - # This can happen when changing channels - self.logger.debug( - f"Received update for channel {live_data.id}, current channel is {current_channel}" - ) - return - - latest_cut_marker = live_data.get_latest_cut() - - if latest_cut_marker: - latest_cut = latest_cut_marker.cut - title = latest_cut.title - artists = ", ".join([a.name for a in latest_cut.artists]) - - self._current_stream_details.stream_title = f"{title} - {artists}" - - async def _refresh_channels(self) -> bool: - self._channels = await self._client.channels - - self._channels_by_id = {} - - for channel in self._channels: - self._channels_by_id[channel.id] = channel - - return True - - def _parse_radio(self, channel: XMChannel) -> Radio: - radio = Radio( - provider=self.instance_id, - item_id=channel.id, - name=channel.name, - provider_mappings={ - ProviderMapping( - provider_domain=self.domain, - provider_instance=self.instance_id, - item_id=channel.id, - ) - }, - ) - - icon = next((i.url for i in channel.images if i.width == 300 and i.height == 300), None) - banner = next( - (i.url for i in channel.images if i.name in ("channel hero image", "background")), None - ) - - images: list[MediaItemImage] = [] - - if icon is not None: - images.append( - MediaItemImage( - provider=self.instance_id, - type=ImageType.THUMB, - path=icon, - remotely_accessible=True, - ) - ) - images.append( - MediaItemImage( - provider=self.instance_id, - type=ImageType.LOGO, - path=icon, - remotely_accessible=True, - ) - ) - - if banner is not None: - images.append( - MediaItemImage( - provider=self.instance_id, - type=ImageType.BANNER, - path=banner, - remotely_accessible=True, - ) - ) - images.append( - MediaItemImage( - provider=self.instance_id, - type=ImageType.LANDSCAPE, - path=banner, - remotely_accessible=True, - ) - ) - - radio.metadata.images = images - radio.metadata.links = [MediaItemLink(type=LinkType.WEBSITE, url=channel.url)] - radio.metadata.description = channel.medium_description - radio.metadata.explicit = bool(channel.is_mature) - radio.metadata.genres = [cat.name for cat in channel.categories] - - return radio diff --git a/music_assistant/server/providers/siriusxm/icon.svg b/music_assistant/server/providers/siriusxm/icon.svg deleted file mode 100644 index bb26dc74..00000000 --- a/music_assistant/server/providers/siriusxm/icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - SIRI_BIG copy-svg - - - diff --git a/music_assistant/server/providers/siriusxm/icon_dark.svg b/music_assistant/server/providers/siriusxm/icon_dark.svg deleted file mode 100644 index fe3bcf3b..00000000 --- a/music_assistant/server/providers/siriusxm/icon_dark.svg +++ /dev/null @@ -1,7 +0,0 @@ - - SIRI_BIG copy-svg - - - diff --git a/music_assistant/server/providers/siriusxm/manifest.json b/music_assistant/server/providers/siriusxm/manifest.json deleted file mode 100644 index d1a36b58..00000000 --- a/music_assistant/server/providers/siriusxm/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "siriusxm", - "name": "SiriusXM", - "description": "Support for the SiriusXM streaming radio provider in Music Assistant.", - "codeowners": ["@btoconnor"], - "requirements": ["sxm==0.2.8"], - "documentation": "https://music-assistant.io/music-providers/siriusxm/", - "multi_instance": false -} diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py deleted file mode 100644 index f78d2b9a..00000000 --- a/music_assistant/server/providers/slimproto/__init__.py +++ /dev/null @@ -1,974 +0,0 @@ -"""Base/builtin provider with support for players using slimproto.""" - -from __future__ import annotations - -import asyncio -import logging -import statistics -import time -from collections import deque -from collections.abc import Iterator -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from aiohttp import web -from aioslimproto.client import PlayerState as SlimPlayerState -from aioslimproto.client import SlimClient -from aioslimproto.client import TransitionType as SlimTransition -from aioslimproto.models import EventType as SlimEventType -from aioslimproto.models import Preset as SlimPreset -from aioslimproto.models import VisualisationType as SlimVisualisationType -from aioslimproto.server import SlimServer - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_EQ_BASS, - CONF_ENTRY_EQ_MID, - CONF_ENTRY_EQ_TREBLE, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, - CONF_ENTRY_OUTPUT_CHANNELS, - CONF_ENTRY_SYNC_ADJUST, - ConfigEntry, - ConfigValueOption, - ConfigValueType, - PlayerConfig, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - MediaType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, - RepeatMode, -) -from music_assistant.common.models.errors import MusicAssistantError, SetupFailedError -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import ( - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_ENFORCE_MP3, - CONF_PORT, - CONF_SYNC_ADJUST, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.helpers.audio import get_ffmpeg_stream, get_player_filter_params -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.player_group import PlayerGroupProvider - -from .multi_client_stream import MultiClientStream - -if TYPE_CHECKING: - from aioslimproto.models import SlimEvent - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -CACHE_KEY_PREV_STATE = "slimproto_prev_state" - - -STATE_MAP = { - SlimPlayerState.BUFFERING: PlayerState.PLAYING, - SlimPlayerState.BUFFER_READY: PlayerState.PLAYING, - SlimPlayerState.PAUSED: PlayerState.PAUSED, - SlimPlayerState.PLAYING: PlayerState.PLAYING, - SlimPlayerState.STOPPED: PlayerState.IDLE, -} -REPEATMODE_MAP = {RepeatMode.OFF: 0, RepeatMode.ONE: 1, RepeatMode.ALL: 2} - -# sync constants -MIN_DEVIATION_ADJUST = 8 # 5 milliseconds -MIN_REQ_PLAYPOINTS = 8 # we need at least 8 measurements -DEVIATION_JUMP_IGNORE = 500 # ignore a sudden unrealistic jump -MAX_SKIP_AHEAD_MS = 800 # 0.8 seconds - - -@dataclass -class SyncPlayPoint: - """Simple structure to describe a Sync Playpoint.""" - - timestamp: float - sync_master: str - diff: int - - -CONF_CLI_TELNET_PORT = "cli_telnet_port" -CONF_CLI_JSON_PORT = "cli_json_port" -CONF_DISCOVERY = "discovery" -CONF_DISPLAY = "display" -CONF_VISUALIZATION = "visualization" - -DEFAULT_PLAYER_VOLUME = 20 -DEFAULT_SLIMPROTO_PORT = 3483 -DEFAULT_VISUALIZATION = SlimVisualisationType.NONE - - -CONF_ENTRY_DISPLAY = ConfigEntry( - key=CONF_DISPLAY, - type=ConfigEntryType.BOOLEAN, - default_value=False, - required=False, - label="Enable display support", - description="Enable/disable native display support on squeezebox or squeezelite32 hardware.", - category="advanced", -) -CONF_ENTRY_VISUALIZATION = ConfigEntry( - key=CONF_VISUALIZATION, - type=ConfigEntryType.STRING, - default_value=DEFAULT_VISUALIZATION, - options=tuple( - ConfigValueOption(title=x.name.replace("_", " ").title(), value=x.value) - for x in SlimVisualisationType - ), - required=False, - label="Visualization type", - description="The type of visualization to show on the display " - "during playback if the device supports this.", - category="advanced", - depends_on=CONF_DISPLAY, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return SlimprotoProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_CLI_TELNET_PORT, - type=ConfigEntryType.INTEGER, - default_value=9090, - label="Classic Squeezebox CLI Port", - description="Some slimproto based players require the presence of the telnet CLI " - " to request more information. \n\n" - "By default this CLI is hosted on port 9090 but some players also accept " - "a different port. Set to 0 to disable this functionality.\n\n" - "Commands allowed on this interface are very limited and just enough to satisfy " - "player compatibility, so security risks are minimized to practically zero." - "You may safely disable this option if you have no players that rely on this feature " - "or you dont care about the additional metadata.", - category="advanced", - ), - ConfigEntry( - key=CONF_CLI_JSON_PORT, - type=ConfigEntryType.INTEGER, - default_value=9000, - label="JSON-RPC CLI/API Port", - description="Some slimproto based players require the presence of the JSON-RPC " - "API from LMS to request more information. For example to fetch the album cover " - "and other metadata. \n\n" - "This JSON-RPC API is compatible with Logitech Media Server but not all commands " - "are implemented. Just enough to satisfy player compatibility. \n\n" - "By default this JSON CLI is hosted on port 9000 but most players also accept " - "it on a different port. Set to 0 to disable this functionality.\n\n" - "You may safely disable this option if you have no players that rely on this feature " - "or you dont care about the additional metadata.", - category="advanced", - ), - ConfigEntry( - key=CONF_DISCOVERY, - type=ConfigEntryType.BOOLEAN, - default_value=True, - label="Enable Discovery server", - description="Broadcast discovery packets for slimproto clients to automatically " - "discover and connect to this server. \n\n" - "You may want to disable this feature if you are running multiple slimproto servers " - "on your network and/or you don't want clients to auto connect to this server.", - category="advanced", - ), - ConfigEntry( - key=CONF_PORT, - type=ConfigEntryType.INTEGER, - default_value=DEFAULT_SLIMPROTO_PORT, - label="Slimproto port", - description="The TCP/UDP port to run the slimproto sockets server. " - "The default is 3483 and using a different port is not supported by " - "hardware squeezebox players. Only adjust this port if you want to " - "use other slimproto based servers side by side with (squeezelite) software players.", - category="advanced", - ), - ) - - -class SlimprotoProvider(PlayerProvider): - """Base/builtin provider for players using the SLIM protocol (aka slimproto).""" - - slimproto: SlimServer - _sync_playpoints: dict[str, deque[SyncPlayPoint]] - _do_not_resync_before: dict[str, float] - _multi_streams: dict[str, MultiClientStream] - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self._sync_playpoints = {} - self._do_not_resync_before = {} - self._multi_streams = {} - control_port = self.config.get_value(CONF_PORT) - telnet_port = self.config.get_value(CONF_CLI_TELNET_PORT) - json_port = self.config.get_value(CONF_CLI_JSON_PORT) - # silence aioslimproto logger a bit - if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("aioslimproto").setLevel(logging.DEBUG) - else: - logging.getLogger("aioslimproto").setLevel(self.logger.level + 10) - self.slimproto = SlimServer( - cli_port=telnet_port or None, - cli_port_json=json_port or None, - ip_address=self.mass.streams.publish_ip, - name="Music Assistant", - control_port=control_port, - ) - # start slimproto socket server - try: - await self.slimproto.start() - except OSError as err: - raise SetupFailedError( - "Unable to start the Slimproto server - " - "is one of the required TCP ports already taken ?" - ) from err - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - await super().loaded_in_mass() - self.slimproto.subscribe(self._client_callback) - self.mass.streams.register_dynamic_route( - "/slimproto/multi", self._serve_multi_client_stream - ) - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - self.mass.streams.unregister_dynamic_route("/slimproto/multi") - await self.slimproto.stop() - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - base_entries = await super().get_player_config_entries(player_id) - if slimclient := self.slimproto.get_player(player_id): - max_sample_rate = int(slimclient.max_sample_rate) - else: - # player not (yet) connected? use default - max_sample_rate = 48000 - # create preset entries (for players that support it) - preset_entries = () - presets = [] - async for playlist in self.mass.music.playlists.iter_library_items(True): - presets.append(ConfigValueOption(playlist.name, playlist.uri)) - async for radio in self.mass.music.radio.iter_library_items(True): - presets.append(ConfigValueOption(radio.name, radio.uri)) - preset_count = 10 - preset_entries = tuple( - ConfigEntry( - key=f"preset_{index}", - type=ConfigEntryType.STRING, - options=presets, - label=f"Preset {index}", - description="Assign a playable item to the player's preset. " - "Only supported on real squeezebox hardware or jive(lite) based emulators.", - category="presets", - required=False, - ) - for index in range(1, preset_count + 1) - ) - return ( - base_entries - + preset_entries - + ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_EQ_BASS, - CONF_ENTRY_EQ_MID, - CONF_ENTRY_EQ_TREBLE, - CONF_ENTRY_OUTPUT_CHANNELS, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_SYNC_ADJUST, - CONF_ENTRY_DISPLAY, - CONF_ENTRY_VISUALIZATION, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, - create_sample_rates_config_entry(max_sample_rate, 24, 48000, 24), - ) - ) - - async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None: - """Call (by config manager) when the configuration of a player changes.""" - if slimplayer := self.slimproto.get_player(config.player_id): - await self._set_preset_items(slimplayer) - await self._set_display(slimplayer) - await super().on_player_config_change(config, changed_keys) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - # forward command to player and any connected sync members - async with TaskManager(self.mass) as tg: - for slimplayer in self._get_sync_clients(player_id): - tg.create_task(slimplayer.stop()) - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - # forward command to player and any connected sync members - async with TaskManager(self.mass) as tg: - for slimplayer in self._get_sync_clients(player_id): - tg.create_task(slimplayer.play()) - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - player = self.mass.players.get(player_id) - if player.synced_to: - msg = "A synced player cannot receive play commands directly" - raise RuntimeError(msg) - - if not player.group_childs: - slimplayer = self.slimproto.get_player(player_id) - # simple, single-player playback - await self._handle_play_url( - slimplayer, - url=media.uri, - media=media, - send_flush=True, - auto_play=False, - ) - return - - # this is a syncgroup, we need to handle this with a multi client stream - master_audio_format = AudioFormat( - content_type=ContentType.PCM_F32LE, - sample_rate=48000, - bit_depth=32, - ) - if media.media_type == MediaType.ANNOUNCEMENT: - # special case: stream announcement - audio_source = self.mass.streams.get_announcement_stream( - media.custom_data["url"], - output_format=master_audio_format, - use_pre_announce=media.custom_data["use_pre_announce"], - ) - elif media.queue_id.startswith("ugp_"): - # special case: UGP stream - ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") - ugp_stream = ugp_provider.ugp_streams[media.queue_id] - audio_source = ugp_stream.subscribe() - elif media.queue_id and media.queue_item_id: - # regular queue stream request - audio_source = self.mass.streams.get_flow_stream( - queue=self.mass.player_queues.get(media.queue_id), - start_queue_item=self.mass.player_queues.get_item( - media.queue_id, media.queue_item_id - ), - pcm_format=master_audio_format, - ) - else: - # assume url or some other direct path - # NOTE: this will fail if its an uri not playable by ffmpeg - audio_source = get_ffmpeg_stream( - audio_input=media.uri, - input_format=AudioFormat(ContentType.try_parse(media.uri)), - output_format=master_audio_format, - ) - # start the stream task - self._multi_streams[player_id] = stream = MultiClientStream( - audio_source=audio_source, audio_format=master_audio_format - ) - base_url = f"{self.mass.streams.base_url}/slimproto/multi?player_id={player_id}&fmt=flac" - - # forward to downstream play_media commands - async with TaskManager(self.mass) as tg: - for slimplayer in self._get_sync_clients(player_id): - url = f"{base_url}&child_player_id={slimplayer.player_id}" - if self.mass.config.get_raw_player_config_value( - slimplayer.player_id, CONF_ENFORCE_MP3, False - ): - url = url.replace("flac", "mp3") - stream.expected_clients += 1 - tg.create_task( - self._handle_play_url( - slimplayer, - url=url, - media=media, - send_flush=True, - auto_play=False, - ) - ) - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next queue item on the player.""" - if not (slimplayer := self.slimproto.get_player(player_id)): - return - url = media.uri - if self.mass.config.get_raw_player_config_value( - slimplayer.player_id, CONF_ENFORCE_MP3, False - ): - url = url.replace("flac", "mp3") - - await self._handle_play_url( - slimplayer, - url=url, - media=media, - enqueue=True, - send_flush=False, - auto_play=True, - ) - - async def _handle_play_url( - self, - slimplayer: SlimClient, - url: str, - media: PlayerMedia, - enqueue: bool = False, - send_flush: bool = True, - auto_play: bool = False, - ) -> None: - """Handle playback of an url on slimproto player(s).""" - player_id = slimplayer.player_id - if crossfade := await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE): - transition_duration = await self.mass.config.get_player_config_value( - player_id, CONF_CROSSFADE_DURATION - ) - else: - transition_duration = 0 - - metadata = { - "item_id": media.uri, - "title": media.title, - "album": media.album, - "artist": media.artist, - "image_url": media.image_url, - "duration": media.duration, - "queue_id": media.queue_id, - "queue_item_id": media.queue_item_id, - } - queue = self.mass.player_queues.get(media.queue_id or player_id) - slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] - slimplayer.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) - await slimplayer.play_url( - url=url, - mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", - metadata=metadata, - enqueue=enqueue, - send_flush=send_flush, - transition=SlimTransition.CROSSFADE if crossfade else SlimTransition.NONE, - transition_duration=transition_duration, - # if autoplay=False playback will not start automatically - # instead 'buffer ready' will be called when the buffer is full - # to coordinate a start of multiple synced players - autostart=auto_play, - ) - # if queue is set to single track repeat, - # immediately set this track as the next - # this prevents race conditions with super short audio clips (on single repeat) - # https://github.com/music-assistant/hass-music-assistant/issues/2059 - if queue.repeat_mode == RepeatMode.ONE: - self.mass.call_later( - 0.2, - slimplayer.play_url( - url=url, - mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", - metadata=metadata, - enqueue=True, - send_flush=False, - transition=SlimTransition.CROSSFADE if crossfade else SlimTransition.NONE, - transition_duration=transition_duration, - autostart=True, - ), - ) - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - # forward command to player and any connected sync members - async with TaskManager(self.mass) as tg: - for slimplayer in self._get_sync_clients(player_id): - tg.create_task(slimplayer.pause()) - - async def cmd_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player.""" - if slimplayer := self.slimproto.get_player(player_id): - await slimplayer.power(powered) - # store last state in cache - await self.mass.cache.set( - player_id, (powered, slimplayer.volume_level), base_key=CACHE_KEY_PREV_STATE - ) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - if slimplayer := self.slimproto.get_player(player_id): - await slimplayer.volume_set(volume_level) - # store last state in cache - await self.mass.cache.set( - player_id, (slimplayer.powered, volume_level), base_key=CACHE_KEY_PREV_STATE - ) - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - if slimplayer := self.slimproto.get_player(player_id): - await slimplayer.mute(muted) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player.""" - child_player = self.mass.players.get(player_id) - assert child_player # guard - parent_player = self.mass.players.get(target_player) - assert parent_player # guard - if parent_player.synced_to: - raise RuntimeError("Parent player is already synced!") - if child_player.synced_to and child_player.synced_to != target_player: - raise RuntimeError("Player is already synced to another player") - # always make sure that the parent player is part of the sync group - parent_player.group_childs.add(parent_player.player_id) - parent_player.group_childs.add(child_player.player_id) - child_player.synced_to = parent_player.player_id - # check if we should (re)start or join a stream session - # TODO: support late joining of a client into an existing stream session - # so it doesn't need to be restarted anymore. - active_queue = self.mass.player_queues.get_active_queue(parent_player.player_id) - if active_queue.state == PlayerState.PLAYING: - # playback needs to be restarted to form a new multi client stream session - # this could potentially be called by multiple players at the exact same time - # so we debounce the resync a bit here with a timer - self.mass.call_later( - 1, - self.mass.player_queues.resume, - active_queue.queue_id, - fade_in=False, - task_id=f"resume_{active_queue.queue_id}", - ) - else: - # make sure that the player manager gets an update - self.mass.players.update(child_player.player_id, skip_forward=True) - self.mass.players.update(parent_player.player_id, skip_forward=True) - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - player = self.mass.players.get(player_id, raise_unavailable=True) - if player.synced_to: - group_leader = self.mass.players.get(player.synced_to, raise_unavailable=True) - if player_id in group_leader.group_childs: - group_leader.group_childs.remove(player_id) - player.synced_to = None - if slimclient := self.slimproto.get_player(player_id): - await slimclient.stop() - # make sure that the player manager gets an update - self.mass.players.update(player.player_id, skip_forward=True) - self.mass.players.update(group_leader.player_id, skip_forward=True) - - def _client_callback( - self, - event: SlimEvent, - ) -> None: - if self.mass.closing: - return - - if event.type == SlimEventType.PLAYER_DISCONNECTED: - if mass_player := self.mass.players.get(event.player_id): - mass_player.available = False - self.mass.players.update(mass_player.player_id) - return - - if not (slimplayer := self.slimproto.get_player(event.player_id)): - return - - if event.type == SlimEventType.PLAYER_CONNECTED: - self.mass.create_task(self._handle_connected(slimplayer)) - return - - if event.type == SlimEventType.PLAYER_BUFFER_READY: - self.mass.create_task(self._handle_buffer_ready(slimplayer)) - return - - if event.type == SlimEventType.PLAYER_HEARTBEAT: - self._handle_player_heartbeat(slimplayer) - return - - if event.type in (SlimEventType.PLAYER_BTN_EVENT, SlimEventType.PLAYER_CLI_EVENT): - self.mass.create_task(self._handle_player_cli_event(slimplayer, event)) - return - - # forward player update to MA player controller - self.mass.create_task(self._handle_player_update(slimplayer)) - - async def _handle_player_update(self, slimplayer: SlimClient) -> None: - """Process SlimClient update/add to Player controller.""" - player_id = slimplayer.player_id - player = self.mass.players.get(player_id, raise_unavailable=False) - if not player: - # player does not yet exist, create it - player = Player( - player_id=player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=slimplayer.name, - available=True, - powered=slimplayer.powered, - device_info=DeviceInfo( - model=slimplayer.device_model, - address=slimplayer.device_address, - manufacturer=slimplayer.device_type, - ), - supported_features=( - PlayerFeature.POWER, - PlayerFeature.SYNC, - PlayerFeature.VOLUME_SET, - PlayerFeature.PAUSE, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.ENQUEUE, - ), - ) - await self.mass.players.register_or_update(player) - - # update player state on player events - player.available = True - if slimplayer.current_media and (metadata := slimplayer.current_media.metadata): - player.current_media = PlayerMedia( - uri=metadata.get("item_id"), - title=metadata.get("title"), - album=metadata.get("album"), - artist=metadata.get("artist"), - image_url=metadata.get("image_url"), - duration=metadata.get("duration"), - queue_id=metadata.get("queue_id"), - queue_item_id=metadata.get("queue_item_id"), - ) - else: - player.current_media = None - player.active_source = player.player_id - player.name = slimplayer.name - player.powered = slimplayer.powered - player.state = STATE_MAP[slimplayer.state] - player.volume_level = slimplayer.volume_level - player.volume_muted = slimplayer.muted - self.mass.players.update(player_id) - - def _handle_player_heartbeat(self, slimplayer: SlimClient) -> None: - """Process SlimClient elapsed_time update.""" - if slimplayer.state == SlimPlayerState.STOPPED: - # ignore server heartbeats when stopped - return - - # elapsed time change on the player will be auto picked up - # by the player manager. - if not (player := self.mass.players.get(slimplayer.player_id)): - # race condition?! - return - player.elapsed_time = slimplayer.elapsed_seconds - player.elapsed_time_last_updated = time.time() - - # handle sync - if player.synced_to: - self._handle_client_sync(slimplayer) - - async def _handle_player_cli_event(self, slimplayer: SlimClient, event: SlimEvent) -> None: - """Process CLI Event.""" - if not event.data: - return - queue = self.mass.player_queues.get_active_queue(slimplayer.player_id) - if event.data.startswith("button preset_") and event.data.endswith(".single"): - preset_id = event.data.split("preset_")[1].split(".")[0] - preset_index = int(preset_id) - 1 - if len(slimplayer.presets) >= preset_index + 1: - preset = slimplayer.presets[preset_index] - await self.mass.player_queues.play_media(queue.queue_id, preset.uri) - elif event.data == "button repeat": - if queue.repeat_mode == RepeatMode.OFF: - repeat_mode = RepeatMode.ONE - elif queue.repeat_mode == RepeatMode.ONE: - repeat_mode = RepeatMode.ALL - else: - repeat_mode = RepeatMode.OFF - self.mass.player_queues.set_repeat(queue.queue_id, repeat_mode) - slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] - slimplayer.signal_update() - elif event.data == "button shuffle": - self.mass.player_queues.set_shuffle(queue.queue_id, not queue.shuffle_enabled) - slimplayer.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) - slimplayer.signal_update() - elif event.data in ("button jump_fwd", "button fwd"): - await self.mass.player_queues.next(queue.queue_id) - elif event.data in ("button jump_rew", "button rew"): - await self.mass.player_queues.previous(queue.queue_id) - elif event.data.startswith("time "): - # seek request - _, param = event.data.split(" ", 1) - if param.isnumeric(): - await self.mass.player_queues.seek(queue.queue_id, int(param)) - self.logger.debug("CLI Event: %s", event.data) - - def _handle_client_sync(self, slimplayer: SlimClient) -> None: - """Synchronize audio of a sync slimplayer.""" - player = self.mass.players.get(slimplayer.player_id) - sync_master_id = player.synced_to - if not sync_master_id: - # we only correct sync members, not the sync master itself - return - if not (sync_master := self.slimproto.get_player(sync_master_id)): - return # just here as a guard as bad things can happen - - if sync_master.state != SlimPlayerState.PLAYING: - return - if slimplayer.state != SlimPlayerState.PLAYING: - return - if slimplayer.player_id not in self._sync_playpoints: - return - - # we collect a few playpoints of the player to determine - # average lag/drift so we can adjust accordingly - sync_playpoints = self._sync_playpoints[slimplayer.player_id] - - now = time.time() - if now < self._do_not_resync_before[slimplayer.player_id]: - return - - last_playpoint = sync_playpoints[-1] if sync_playpoints else None - if last_playpoint and (now - last_playpoint.timestamp) > 10: - # last playpoint is too old, invalidate - sync_playpoints.clear() - if last_playpoint and last_playpoint.sync_master != sync_master.player_id: - # this should not happen, but just in case - sync_playpoints.clear() - - diff = int( - self._get_corrected_elapsed_milliseconds(sync_master) - - self._get_corrected_elapsed_milliseconds(slimplayer) - ) - - # ignore unexpected spikes - if ( - sync_playpoints - and abs(statistics.fmean(x.diff for x in sync_playpoints)) > DEVIATION_JUMP_IGNORE - ): - return - - # we can now append the current playpoint to our list - sync_playpoints.append(SyncPlayPoint(now, sync_master.player_id, diff)) - - min_req_playpoints = 2 if sync_master.elapsed_seconds < 2 else MIN_REQ_PLAYPOINTS - if len(sync_playpoints) < min_req_playpoints: - return - - # get the average diff - avg_diff = statistics.fmean(x.diff for x in sync_playpoints) - delta = int(abs(avg_diff)) - - if delta < MIN_DEVIATION_ADJUST: - return - - # resync the player by skipping ahead or pause for x amount of (milli)seconds - sync_playpoints.clear() - self._do_not_resync_before[player.player_id] = now + 5 - if avg_diff > MAX_SKIP_AHEAD_MS: - # player lagging behind more than MAX_SKIP_AHEAD_MS, - # we need to correct the sync_master - self.logger.debug("%s resync: pauseFor %sms", sync_master.name, delta) - self.mass.create_task(sync_master.pause_for(delta)) - elif avg_diff > 0: - # handle player lagging behind, fix with skip_ahead - self.logger.debug("%s resync: skipAhead %sms", player.display_name, delta) - self.mass.create_task(slimplayer.skip_over(delta)) - else: - # handle player is drifting too far ahead, use pause_for to adjust - self.logger.debug("%s resync: pauseFor %sms", player.display_name, delta) - self.mass.create_task(slimplayer.pause_for(delta)) - - async def _handle_buffer_ready(self, slimplayer: SlimClient) -> None: - """Handle buffer ready event, player has buffered a (new) track. - - Only used when autoplay=0 for coordinated start of synced players. - """ - player = self.mass.players.get(slimplayer.player_id) - if player.synced_to: - # unpause of sync child is handled by sync master - return - if not player.group_childs: - # not a sync group, continue - await slimplayer.unpause_at(slimplayer.jiffies) - return - count = 0 - while count < 40: - childs_total = 0 - childs_ready = 0 - await asyncio.sleep(0.2) - for sync_child in self._get_sync_clients(player.player_id): - childs_total += 1 - if sync_child.state == SlimPlayerState.BUFFER_READY: - childs_ready += 1 - if childs_total == childs_ready: - break - - # all child's ready (or timeout) - start play - async with TaskManager(self.mass) as tg: - for _client in self._get_sync_clients(player.player_id): - self._sync_playpoints.setdefault( - _client.player_id, deque(maxlen=MIN_REQ_PLAYPOINTS) - ).clear() - # NOTE: Officially you should do an unpause_at based on the player timestamp - # but I did not have any good results with that. - # Instead just start playback on all players and let the sync logic work out - # the delays etc. - self._do_not_resync_before[_client.player_id] = time.time() + 1 - tg.create_task(_client.pause_for(200)) - - async def _handle_connected(self, slimplayer: SlimClient) -> None: - """Handle a slimplayer connected event.""" - player_id = slimplayer.player_id - self.logger.info("Player %s connected", slimplayer.name or player_id) - # set presets and display - await self._set_preset_items(slimplayer) - await self._set_display(slimplayer) - # update all attributes - await self._handle_player_update(slimplayer) - # restore volume and power state - if last_state := await self.mass.cache.get(player_id, base_key=CACHE_KEY_PREV_STATE): - init_power = last_state[0] - init_volume = last_state[1] - else: - init_volume = DEFAULT_PLAYER_VOLUME - init_power = False - await slimplayer.power(init_power) - await slimplayer.stop() - await slimplayer.volume_set(init_volume) - - def _get_sync_clients(self, player_id: str) -> Iterator[SlimClient]: - """Get all sync clients for a player.""" - player = self.mass.players.get(player_id) - # we need to return the player itself too - group_child_ids = {player_id} - group_child_ids.update(player.group_childs) - for child_id in group_child_ids: - if slimplayer := self.slimproto.get_player(child_id): - yield slimplayer - - def _get_corrected_elapsed_milliseconds(self, slimplayer: SlimClient) -> int: - """Return corrected elapsed milliseconds.""" - sync_delay = self.mass.config.get_raw_player_config_value( - slimplayer.player_id, CONF_SYNC_ADJUST, 0 - ) - return slimplayer.elapsed_milliseconds - sync_delay - - async def _set_preset_items(self, slimplayer: SlimClient) -> None: - """Set the presets for a player.""" - preset_items: list[SlimPreset] = [] - for preset_index in range(1, 11): - if preset_conf := self.mass.config.get_raw_player_config_value( - slimplayer.player_id, f"preset_{preset_index}" - ): - try: - media_item = await self.mass.music.get_item_by_uri(preset_conf) - preset_items.append( - SlimPreset( - uri=media_item.uri, - text=media_item.name, - icon=self.mass.metadata.get_image_url(media_item.image), - ) - ) - except MusicAssistantError: - # non-existing media item or some other edge case - preset_items.append( - SlimPreset( - uri=f"preset_{preset_index}", - text=f"ERROR ", - icon="", - ) - ) - else: - break - slimplayer.presets = preset_items - - async def _set_display(self, slimplayer: SlimClient) -> None: - """Set the display config for a player.""" - display_enabled = self.mass.config.get_raw_player_config_value( - slimplayer.player_id, - CONF_ENTRY_DISPLAY.key, - CONF_ENTRY_DISPLAY.default_value, - ) - visualization = self.mass.config.get_raw_player_config_value( - slimplayer.player_id, - CONF_ENTRY_VISUALIZATION.key, - CONF_ENTRY_VISUALIZATION.default_value, - ) - await slimplayer.configure_display( - visualisation=SlimVisualisationType(visualization), disabled=not display_enabled - ) - - async def _serve_multi_client_stream(self, request: web.Request) -> web.Response: - """Serve the multi-client flow stream audio to a player.""" - player_id = request.query.get("player_id") - fmt = request.query.get("fmt") - child_player_id = request.query.get("child_player_id") - - if not self.mass.players.get(player_id): - raise web.HTTPNotFound(reason=f"Unknown player: {player_id}") - - if not (child_player := self.mass.players.get(child_player_id)): - raise web.HTTPNotFound(reason=f"Unknown player: {child_player_id}") - - if not (stream := self._multi_streams.get(player_id, None)) or stream.done: - raise web.HTTPNotFound(f"There is no active stream for {player_id}!") - - resp = web.StreamResponse( - status=200, - reason="OK", - headers={ - "Content-Type": f"audio/{fmt}", - }, - ) - await resp.prepare(request) - - # return early if this is not a GET request - if request.method != "GET": - return resp - - # all checks passed, start streaming! - self.logger.debug( - "Start serving multi-client flow audio stream to %s", - child_player.display_name, - ) - - async for chunk in stream.get_stream( - output_format=AudioFormat(content_type=ContentType.try_parse(fmt)), - filter_params=get_player_filter_params(self.mass, child_player_id) - if child_player_id - else None, - ): - try: - await resp.write(chunk) - except (BrokenPipeError, ConnectionResetError, ConnectionError): - # race condition - break - - return resp diff --git a/music_assistant/server/providers/slimproto/icon.svg b/music_assistant/server/providers/slimproto/icon.svg deleted file mode 100644 index 20dc9b94..00000000 --- a/music_assistant/server/providers/slimproto/icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/music_assistant/server/providers/slimproto/manifest.json b/music_assistant/server/providers/slimproto/manifest.json deleted file mode 100644 index 86bda8f4..00000000 --- a/music_assistant/server/providers/slimproto/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "player", - "domain": "slimproto", - "name": "Slimproto (Squeezebox players)", - "description": "Support for slimproto based players (e.g. squeezebox, squeezelite).", - "codeowners": ["@music-assistant"], - "requirements": ["aioslimproto==3.1.0"], - "documentation": "https://music-assistant.io/player-support/slimproto/", - "multi_instance": false, - "builtin": false -} diff --git a/music_assistant/server/providers/slimproto/multi_client_stream.py b/music_assistant/server/providers/slimproto/multi_client_stream.py deleted file mode 100644 index 5ba2a53b..00000000 --- a/music_assistant/server/providers/slimproto/multi_client_stream.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Implementation of a simple multi-client stream task/job.""" - -import asyncio -import logging -from collections.abc import AsyncGenerator -from contextlib import suppress - -from music_assistant.common.helpers.util import empty_queue -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.server.helpers.audio import get_ffmpeg_stream - -LOGGER = logging.getLogger(__name__) - - -class MultiClientStream: - """Implementation of a simple multi-client (audio) stream task/job.""" - - def __init__( - self, - audio_source: AsyncGenerator[bytes, None], - audio_format: AudioFormat, - expected_clients: int = 0, - ) -> None: - """Initialize MultiClientStream.""" - self.audio_source = audio_source - self.audio_format = audio_format - self.subscribers: list[asyncio.Queue] = [] - self.expected_clients = expected_clients - self.task = asyncio.create_task(self._runner()) - - @property - def done(self) -> bool: - """Return if this stream is already done.""" - return self.task.done() - - async def stop(self) -> None: - """Stop/cancel the stream.""" - if self.done: - return - self.task.cancel() - with suppress(asyncio.CancelledError): - await self.task - for sub_queue in list(self.subscribers): - empty_queue(sub_queue) - - async def get_stream( - self, - output_format: AudioFormat, - filter_params: list[str] | None = None, - ) -> AsyncGenerator[bytes, None]: - """Get (client specific encoded) ffmpeg stream.""" - async for chunk in get_ffmpeg_stream( - audio_input=self.subscribe_raw(), - input_format=self.audio_format, - output_format=output_format, - filter_params=filter_params, - ): - yield chunk - - async def subscribe_raw(self) -> AsyncGenerator[bytes, None]: - """Subscribe to the raw/unaltered audio stream.""" - try: - queue = asyncio.Queue(2) - self.subscribers.append(queue) - while True: - chunk = await queue.get() - if chunk == b"": - break - yield chunk - finally: - with suppress(ValueError): - self.subscribers.remove(queue) - - async def _runner(self) -> None: - """Run the stream for the given audio source.""" - expected_clients = self.expected_clients or 1 - # wait for first/all subscriber - count = 0 - while count < 50: - await asyncio.sleep(0.1) - count += 1 - if len(self.subscribers) >= expected_clients: - break - LOGGER.debug( - "Starting multi-client stream with %s/%s clients", - len(self.subscribers), - self.expected_clients, - ) - async for chunk in self.audio_source: - fail_count = 0 - while len(self.subscribers) == 0: - await asyncio.sleep(0.1) - fail_count += 1 - if fail_count > 50: - LOGGER.warning("No clients connected, stopping stream") - return - await asyncio.gather( - *[sub.put(chunk) for sub in self.subscribers], return_exceptions=True - ) - # EOF: send empty chunk - await asyncio.gather(*[sub.put(b"") for sub in self.subscribers], return_exceptions=True) diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/server/providers/snapcast/__init__.py deleted file mode 100644 index 0cc1b523..00000000 --- a/music_assistant/server/providers/snapcast/__init__.py +++ /dev/null @@ -1,743 +0,0 @@ -"""Snapcast Player provider for Music Assistant.""" - -from __future__ import annotations - -import asyncio -import logging -import pathlib -import random -import re -import socket -import time -from contextlib import suppress -from typing import TYPE_CHECKING, Final, cast - -from bidict import bidict -from snapcast.control import create_server -from snapcast.control.client import Snapclient -from zeroconf import NonUniqueNameException -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.helpers.util import get_ip_pton -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_FLOW_MODE_ENFORCED, - ConfigEntry, - ConfigValueOption, - ConfigValueType, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ContentType, - MediaType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.errors import SetupFailedError -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.audio import FFMpeg, get_ffmpeg_stream, get_player_filter_params -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.models.player_provider import PlayerProvider - -if TYPE_CHECKING: - from snapcast.control.group import Snapgroup - from snapcast.control.server import Snapserver - from snapcast.control.stream import Snapstream - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - from music_assistant.server.providers.player_group import PlayerGroupProvider - -CONF_SERVER_HOST = "snapcast_server_host" -CONF_SERVER_CONTROL_PORT = "snapcast_server_control_port" -CONF_USE_EXTERNAL_SERVER = "snapcast_use_external_server" -CONF_SERVER_BUFFER_SIZE = "snapcast_server_built_in_buffer_size" -CONF_SERVER_CHUNK_MS = "snapcast_server_built_in_chunk_ms" -CONF_SERVER_INITIAL_VOLUME = "snapcast_server_built_in_initial_volume" -CONF_SERVER_TRANSPORT_CODEC = "snapcast_server_built_in_codec" -CONF_SERVER_SEND_AUDIO_TO_MUTED = "snapcast_server_built_in_send_muted" -CONF_STREAM_IDLE_THRESHOLD = "snapcast_stream_idle_threshold" - - -CONF_CATEGORY_GENERIC = "generic" -CONF_CATEGORY_ADVANCED = "advanced" -CONF_CATEGORY_BUILT_IN = "Built-in Snapserver Settings" - -CONF_HELP_LINK = ( - "https://raw.githubusercontent.com/badaix/snapcast/refs/heads/master/server/etc/snapserver.conf" -) - -# airplay has fixed sample rate/bit depth so make this config entry static and hidden -CONF_ENTRY_SAMPLE_RATES_SNAPCAST = create_sample_rates_config_entry(48000, 16, 48000, 16, True) - -DEFAULT_SNAPSERVER_IP = "127.0.0.1" -DEFAULT_SNAPSERVER_PORT = 1705 -DEFAULT_SNAPSTREAM_IDLE_THRESHOLD = 60000 - -SNAPWEB_DIR: Final[pathlib.Path] = pathlib.Path(__file__).parent.resolve().joinpath("snapweb") - - -DEFAULT_SNAPCAST_FORMAT = AudioFormat( - content_type=ContentType.PCM_S16LE, - sample_rate=48000, - # TODO: can we handle 24 bits bit depth ? - bit_depth=16, - channels=2, -) - -DEFAULT_SNAPCAST_PCM_FORMAT = AudioFormat( - # the format that is used as intermediate pcm stream, - # we prefer F32 here to account for volume normalization - content_type=ContentType.PCM_F32LE, - sample_rate=48000, - bit_depth=32, - channels=2, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return SnapCastProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, # noqa: ARG001 - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - returncode, output = await check_output("snapserver", "-v") - snapserver_version = int(output.decode().split(".")[1]) if returncode == 0 else -1 - local_snapserver_present = snapserver_version >= 27 - if returncode == 0 and not local_snapserver_present: - raise SetupFailedError("Invalid snapserver version") - - return ( - ConfigEntry( - key=CONF_SERVER_BUFFER_SIZE, - type=ConfigEntryType.INTEGER, - range=(200, 6000), - default_value=1000, - label="Snapserver buffer size", - required=False, - category=CONF_CATEGORY_BUILT_IN, - hidden=not local_snapserver_present, - help_link=CONF_HELP_LINK, - ), - ConfigEntry( - key=CONF_SERVER_CHUNK_MS, - type=ConfigEntryType.INTEGER, - range=(10, 100), - default_value=26, - label="Snapserver chunk size", - required=False, - category=CONF_CATEGORY_BUILT_IN, - hidden=not local_snapserver_present, - help_link=CONF_HELP_LINK, - ), - ConfigEntry( - key=CONF_SERVER_INITIAL_VOLUME, - type=ConfigEntryType.INTEGER, - range=(0, 100), - default_value=25, - label="Snapserver initial volume", - required=False, - category=CONF_CATEGORY_BUILT_IN, - hidden=not local_snapserver_present, - help_link=CONF_HELP_LINK, - ), - ConfigEntry( - key=CONF_SERVER_SEND_AUDIO_TO_MUTED, - type=ConfigEntryType.BOOLEAN, - default_value=False, - label="Send audio to muted clients", - required=False, - category=CONF_CATEGORY_BUILT_IN, - hidden=not local_snapserver_present, - help_link=CONF_HELP_LINK, - ), - ConfigEntry( - key=CONF_SERVER_TRANSPORT_CODEC, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption( - title="FLAC", - value="flac", - ), - ConfigValueOption( - title="OGG", - value="ogg", - ), - ConfigValueOption( - title="OPUS", - value="opus", - ), - ConfigValueOption( - title="PCM", - value="pcm", - ), - ), - default_value="flac", - label="Snapserver default transport codec", - required=False, - category=CONF_CATEGORY_BUILT_IN, - hidden=not local_snapserver_present, - help_link=CONF_HELP_LINK, - ), - ConfigEntry( - key=CONF_USE_EXTERNAL_SERVER, - type=ConfigEntryType.BOOLEAN, - default_value=not local_snapserver_present, - label="Use existing Snapserver", - required=False, - category=( - CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC - ), - ), - ConfigEntry( - key=CONF_SERVER_HOST, - type=ConfigEntryType.STRING, - default_value=DEFAULT_SNAPSERVER_IP, - label="Snapcast server ip", - required=False, - depends_on=CONF_USE_EXTERNAL_SERVER, - category=( - CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC - ), - ), - ConfigEntry( - key=CONF_SERVER_CONTROL_PORT, - type=ConfigEntryType.INTEGER, - default_value=DEFAULT_SNAPSERVER_PORT, - label="Snapcast control port", - required=False, - depends_on=CONF_USE_EXTERNAL_SERVER, - category=( - CONF_CATEGORY_ADVANCED if local_snapserver_present else CONF_CATEGORY_GENERIC - ), - ), - ConfigEntry( - key=CONF_STREAM_IDLE_THRESHOLD, - type=ConfigEntryType.INTEGER, - default_value=DEFAULT_SNAPSTREAM_IDLE_THRESHOLD, - label="Snapcast idle threshold stream parameter", - required=True, - category=CONF_CATEGORY_ADVANCED, - ), - ) - - -class SnapCastProvider(PlayerProvider): - """Player provider for Snapcast based players.""" - - _snapserver: Snapserver - _snapcast_server_host: str - _snapcast_server_control_port: int - _stream_tasks: dict[str, asyncio.Task] - _use_builtin_server: bool - _snapserver_runner: asyncio.Task | None - _snapserver_started: asyncio.Event | None - _ids_map: bidict # ma_id / snapclient_id - _stop_called: bool - - def _get_snapclient_id(self, player_id: str) -> str: - search_dict = self._ids_map - return search_dict.get(player_id) - - def _get_ma_id(self, snap_client_id: str) -> str: - search_dict = self._ids_map.inverse - return search_dict.get(snap_client_id) - - def _generate_and_register_id(self, snap_client_id) -> str: - search_dict = self._ids_map.inverse - if snap_client_id not in search_dict: - new_id = "ma_" + str(re.sub(r"\W+", "", snap_client_id)) - self._ids_map[new_id] = snap_client_id - return new_id - else: - return self._get_ma_id(snap_client_id) - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - # set snapcast logging - logging.getLogger("snapcast").setLevel(self.logger.level) - self._use_builtin_server = not self.config.get_value(CONF_USE_EXTERNAL_SERVER) - self._stop_called = False - if self._use_builtin_server: - self._snapcast_server_host = "127.0.0.1" - self._snapcast_server_control_port = DEFAULT_SNAPSERVER_PORT - self._snapcast_server_buffer_size = self.config.get_value(CONF_SERVER_BUFFER_SIZE) - self._snapcast_server_chunk_ms = self.config.get_value(CONF_SERVER_CHUNK_MS) - self._snapcast_server_initial_volume = self.config.get_value(CONF_SERVER_INITIAL_VOLUME) - self._snapcast_server_send_to_muted = self.config.get_value( - CONF_SERVER_SEND_AUDIO_TO_MUTED - ) - self._snapcast_server_transport_codec = self.config.get_value( - CONF_SERVER_TRANSPORT_CODEC - ) - - else: - self._snapcast_server_host = self.config.get_value(CONF_SERVER_HOST) - self._snapcast_server_control_port = self.config.get_value(CONF_SERVER_CONTROL_PORT) - self._snapcast_stream_idle_threshold = self.config.get_value(CONF_STREAM_IDLE_THRESHOLD) - self._stream_tasks = {} - self._ids_map = bidict({}) - - if self._use_builtin_server: - await self._start_builtin_server() - else: - self._snapserver_runner = None - self._snapserver_started = None - try: - self._snapserver = await create_server( - self.mass.loop, - self._snapcast_server_host, - port=self._snapcast_server_control_port, - reconnect=True, - ) - self._snapserver.set_on_update_callback(self._handle_update) - self.logger.info( - "Started connection to Snapserver %s", - f"{self._snapcast_server_host}:{self._snapcast_server_control_port}", - ) - # register callback for when the connection gets lost to the snapserver - self._snapserver.set_on_disconnect_callback(self._handle_disconnect) - await self._create_default_stream() - except OSError as err: - msg = "Unable to start the Snapserver connection ?" - raise SetupFailedError(msg) from err - - async def loaded_in_mass(self) -> None: - """Call after the provider has been loaded.""" - await super().loaded_in_mass() - # initial load of players - self._handle_update() - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - self._stop_called = True - for snap_client_id in self._snapserver.clients: - player_id = self._get_ma_id(snap_client_id) - await self.cmd_stop(player_id) - self._snapserver.stop() - await self._stop_builtin_server() - - def _handle_update(self) -> None: - """Process Snapcast init Player/Group and set callback .""" - for snap_client in self._snapserver.clients: - self._handle_player_init(snap_client) - snap_client.set_callback(self._handle_player_update) - for snap_client in self._snapserver.clients: - self._handle_player_update(snap_client) - for snap_group in self._snapserver.groups: - snap_group.set_callback(self._handle_group_update) - - def _handle_group_update(self, snap_group: Snapgroup) -> None: - """Process Snapcast group callback.""" - for snap_client in self._snapserver.clients: - self._handle_player_update(snap_client) - - def _handle_player_init(self, snap_client: Snapclient) -> None: - """Process Snapcast add to Player controller.""" - player_id = self._generate_and_register_id(snap_client.identifier) - player = self.mass.players.get(player_id, raise_unavailable=False) - if not player: - snap_client = cast( - Snapclient, self._snapserver.client(self._get_snapclient_id(player_id)) - ) - player = Player( - player_id=player_id, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=snap_client.friendly_name, - available=True, - powered=snap_client.connected, - device_info=DeviceInfo( - model=snap_client._client.get("host").get("os"), - address=snap_client._client.get("host").get("ip"), - manufacturer=snap_client._client.get("host").get("arch"), - ), - supported_features=( - PlayerFeature.SYNC, - PlayerFeature.VOLUME_SET, - PlayerFeature.VOLUME_MUTE, - ), - group_childs=set(), - synced_to=self._synced_to(player_id), - ) - asyncio.run_coroutine_threadsafe( - self.mass.players.register_or_update(player), loop=self.mass.loop - ) - - def _handle_player_update(self, snap_client: Snapclient) -> None: - """Process Snapcast update to Player controller.""" - player_id = self._get_ma_id(snap_client.identifier) - player = self.mass.players.get(player_id) - if not player: - return - player.name = snap_client.friendly_name - player.volume_level = snap_client.volume - player.volume_muted = snap_client.muted - player.available = snap_client.connected - player.synced_to = self._synced_to(player_id) - if player.active_group is None: - if stream := self._get_snapstream(player_id): - if stream.name.startswith(("MusicAssistant", "default")): - player.active_source = player_id - else: - player.active_source = stream.name - else: - player.active_source = player_id - self._group_childs(player_id) - self.mass.players.update(player_id) - - async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: - """Return all (provider/player specific) Config Entries for the given player (if any).""" - base_entries = await super().get_player_config_entries(player_id) - return ( - *base_entries, - CONF_ENTRY_FLOW_MODE_ENFORCED, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_CROSSFADE_DURATION, - CONF_ENTRY_SAMPLE_RATES_SNAPCAST, - ) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - snap_client_id = self._get_snapclient_id(player_id) - await self._snapserver.client(snap_client_id).set_volume(volume_level) - self.mass.players.update(snap_client_id) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - player = self.mass.players.get(player_id, raise_unavailable=False) - if stream_task := self._stream_tasks.pop(player_id, None): - if not stream_task.done(): - stream_task.cancel() - player.state = PlayerState.IDLE - self._set_childs_state(player_id) - self.mass.players.update(player_id) - # assign default/empty stream to the player - await self._get_snapgroup(player_id).set_stream("default") - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send MUTE command to given player.""" - ma_player = self.mass.players.get(player_id, raise_unavailable=False) - snap_client_id = self._get_snapclient_id(player_id) - snapclient = self._snapserver.client(snap_client_id) - # Using optimistic value because the library does not return the response from the api - await snapclient.set_muted(muted) - ma_player.volume_muted = snapclient.muted - self.mass.players.update(player_id) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Sync Snapcast player.""" - group = self._get_snapgroup(target_player) - mass_target_player = self.mass.players.get(target_player) - if self._get_snapclient_id(player_id) not in group.clients: - await group.add_client(self._get_snapclient_id(player_id)) - mass_player = self.mass.players.get(player_id) - mass_player.synced_to = target_player - mass_target_player.group_childs.add(player_id) - self.mass.players.update(player_id) - self.mass.players.update(target_player) - - async def cmd_unsync(self, player_id: str) -> None: - """Unsync Snapcast player.""" - mass_player = self.mass.players.get(player_id) - if mass_player.synced_to is None: - for mass_child_id in list(mass_player.group_childs): - if mass_child_id != player_id: - await self.cmd_unsync(mass_child_id) - return - mass_sync_master_player = self.mass.players.get(mass_player.synced_to) - mass_sync_master_player.group_childs.remove(player_id) - mass_player.synced_to = None - snap_client_id = self._get_snapclient_id(player_id) - group = self._get_snapgroup(player_id) - await group.remove_client(snap_client_id) - # assign default/empty stream to the player - await self._get_snapgroup(player_id).set_stream("default") - await self.cmd_stop(player_id=player_id) - # make sure that the player manager gets an update - self.mass.players.update(player_id, skip_forward=True) - self.mass.players.update(mass_player.synced_to, skip_forward=True) - - async def play_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle PLAY MEDIA on given player.""" - player = self.mass.players.get(player_id) - if player.synced_to: - msg = "A synced player cannot receive play commands directly" - raise RuntimeError(msg) - # stop any existing streams first - if stream_task := self._stream_tasks.pop(player_id, None): - if not stream_task.done(): - stream_task.cancel() - # initialize a new stream and attach it to the group - stream, port = await self._create_stream() - snap_group = self._get_snapgroup(player_id) - await snap_group.set_stream(stream.identifier) - - # select audio source - if media.media_type == MediaType.ANNOUNCEMENT: - # special case: stream announcement - input_format = DEFAULT_SNAPCAST_FORMAT - audio_source = self.mass.streams.get_announcement_stream( - media.custom_data["url"], - output_format=DEFAULT_SNAPCAST_FORMAT, - use_pre_announce=media.custom_data["use_pre_announce"], - ) - elif media.queue_id.startswith("ugp_"): - # special case: UGP stream - ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") - ugp_stream = ugp_provider.ugp_streams[media.queue_id] - input_format = ugp_stream.output_format - audio_source = ugp_stream.subscribe() - elif media.queue_id and media.queue_item_id: - # regular queue (flow) stream request - input_format = DEFAULT_SNAPCAST_PCM_FORMAT - audio_source = self.mass.streams.get_flow_stream( - queue=self.mass.player_queues.get(media.queue_id), - start_queue_item=self.mass.player_queues.get_item( - media.queue_id, media.queue_item_id - ), - pcm_format=input_format, - ) - else: - # assume url or some other direct path - # NOTE: this will fail if its an uri not playable by ffmpeg - input_format = DEFAULT_SNAPCAST_FORMAT - audio_source = get_ffmpeg_stream( - audio_input=media.uri, - input_format=AudioFormat(ContentType.try_parse(media.uri)), - output_format=DEFAULT_SNAPCAST_FORMAT, - ) - - async def _streamer() -> None: - host = self._snapcast_server_host - stream_path = f"tcp://{host}:{port}" - self.logger.debug("Start streaming to %s", stream_path) - try: - async with FFMpeg( - audio_input=audio_source, - input_format=input_format, - output_format=DEFAULT_SNAPCAST_FORMAT, - filter_params=get_player_filter_params(self.mass, player_id), - audio_output=stream_path, - ) as ffmpeg_proc: - player.state = PlayerState.PLAYING - player.current_media = media - player.elapsed_time = 0 - player.elapsed_time_last_updated = time.time() - self.mass.players.update(player_id) - self._set_childs_state(player_id) - await ffmpeg_proc.wait() - self.logger.debug("Finished streaming to %s", stream_path) - # we need to wait a bit for the stream status to become idle - # to ensure that all snapclients have consumed the audio - while stream.status != "idle": - await asyncio.sleep(0.25) - player.state = PlayerState.IDLE - self.mass.players.update(player_id) - self._set_childs_state(player_id) - finally: - await self._delete_current_snapstream(stream, media) - - # start streaming the queue (pcm) audio in a background task - self._stream_tasks[player_id] = asyncio.create_task(_streamer()) - - async def _delete_current_snapstream(self, stream: Snapstream, media: PlayerMedia) -> None: - with suppress(TypeError, KeyError, AttributeError): - if media.duration < 5: - await asyncio.sleep(5) - await self._snapserver.stream_remove_stream(stream.identifier) - - def _get_snapgroup(self, player_id: str) -> Snapgroup: - """Get snapcast group for given player_id.""" - snap_client_id = self._get_snapclient_id(player_id) - client: Snapclient = self._snapserver.client(snap_client_id) - return client.group - - def _get_snapstream(self, player_id: str) -> Snapstream | None: - """Get snapcast stream for given player_id.""" - if group := self._get_snapgroup(player_id): - with suppress(KeyError): - return self._snapserver.stream(group.stream) - return None - - def _synced_to(self, player_id: str) -> str | None: - """Return player_id of the player this player is synced to.""" - snap_group: Snapgroup = self._get_snapgroup(player_id) - master_id: str = self._get_ma_id(snap_group.clients[0]) - - if len(snap_group.clients) < 2 or player_id == master_id: - return None - return master_id - - def _group_childs(self, player_id: str) -> set[str]: - """Return player_ids of the players synced to this player.""" - mass_player = self.mass.players.get(player_id, raise_unavailable=False) - snap_group = self._get_snapgroup(player_id) - mass_player.group_childs.clear() - if mass_player.synced_to is not None: - return - mass_player.group_childs.add(player_id) - { - mass_player.group_childs.add(self._get_ma_id(snap_client_id)) - for snap_client_id in snap_group.clients - if self._get_ma_id(snap_client_id) != player_id - and self._snapserver.client(snap_client_id).connected - } - - async def _create_stream(self) -> tuple[Snapstream, int]: - """Create new stream on snapcast server.""" - attempts = 50 - while attempts: - attempts -= 1 - # pick a random port - port = random.randint(4953, 4953 + 200) - name = f"MusicAssistant--{port}" - result = await self._snapserver.stream_add_stream( - # NOTE: setting the sampleformat to something else - # (like 24 bits bit depth) does not seem to work at all! - f"tcp://0.0.0.0:{port}?name={name}&sampleformat=48000:16:2&idle_threshold={self._snapcast_stream_idle_threshold}", - ) - if "id" not in result: - # if the port is already taken, the result will be an error - self.logger.warning(result) - continue - stream = self._snapserver.stream(result["id"]) - return (stream, port) - msg = "Unable to create stream - No free port found?" - raise RuntimeError(msg) - - async def _create_default_stream(self) -> None: - """Create new stream on snapcast server named default case not exist.""" - all_streams = {stream.name for stream in self._snapserver.streams} - if "default" not in all_streams: - await self._snapserver.stream_add_stream( - "pipe:///tmp/snapfifo?name=default&sampleformat=48000:16:2" - ) - - def _set_childs_state(self, player_id: str) -> None: - """Set the state of the child`s of the player.""" - mass_player = self.mass.players.get(player_id) - for child_player_id in mass_player.group_childs: - if child_player_id == player_id: - continue - mass_child_player = self.mass.players.get(child_player_id) - mass_child_player.state = mass_player.state - self.mass.players.update(child_player_id) - - async def _builtin_server_runner(self) -> None: - """Start running the builtin snapserver.""" - if self._snapserver_started.is_set(): - raise RuntimeError("Snapserver is already started!") - logger = self.logger.getChild("snapserver") - logger.info("Starting builtin Snapserver...") - # register the snapcast mdns services - for name, port in ( - ("-http", 1780), - ("-jsonrpc", 1705), - ("-stream", 1704), - ("-tcp", 1705), - ("", 1704), - ): - zeroconf_type = f"_snapcast{name}._tcp.local." - try: - info = AsyncServiceInfo( - zeroconf_type, - name=f"Snapcast.{zeroconf_type}", - properties={"is_mass": "true"}, - addresses=[await get_ip_pton(self.mass.streams.publish_ip)], - port=port, - server=f"{socket.gethostname()}.local", - ) - attr_name = f"zc_service_set{name}" - if getattr(self, attr_name, None): - await self.mass.aiozc.async_update_service(info) - else: - await self.mass.aiozc.async_register_service(info, strict=False) - setattr(self, attr_name, True) - except NonUniqueNameException: - self.logger.debug( - "Could not register mdns record for %s as its already in use", - zeroconf_type, - ) - except Exception as err: - self.logger.exception( - "Could not register mdns record for %s: %s", zeroconf_type, str(err) - ) - args = [ - "snapserver", - # config settings taken from - # https://raw.githubusercontent.com/badaix/snapcast/86cd4b2b63e750a72e0dfe6a46d47caf01426c8d/server/etc/snapserver.conf - f"--server.datadir={self.mass.storage_path}", - "--http.enabled=true", - "--http.port=1780", - f"--http.doc_root={SNAPWEB_DIR}", - "--tcp.enabled=true", - f"--tcp.port={self._snapcast_server_control_port}", - f"--stream.buffer={self._snapcast_server_buffer_size}", - f"--stream.chunk_ms={self._snapcast_server_chunk_ms}", - f"--stream.codec={self._snapcast_server_transport_codec}", - f"--stream.send_to_muted={str(self._snapcast_server_send_to_muted).lower()}", - f"--streaming_client.initial_volume={self._snapcast_server_initial_volume}", - ] - async with AsyncProcess(args, stdout=True, name="snapserver") as snapserver_proc: - # keep reading from stdout until exit - async for data in snapserver_proc.iter_any(): - data = data.decode().strip() # noqa: PLW2901 - for line in data.split("\n"): - logger.debug(line) - if "(Snapserver) Version 0." in line: - # delay init a small bit to prevent race conditions - # where we try to connect too soon - self.mass.loop.call_later(2, self._snapserver_started.set) - - async def _stop_builtin_server(self) -> None: - """Stop the built-in Snapserver.""" - self.logger.info("Stopping, built-in Snapserver") - if self._snapserver_runner and not self._snapserver_runner.done(): - self._snapserver_runner.cancel() - self._snapserver_started.clear() - - async def _start_builtin_server(self) -> None: - """Start the built-in Snapserver.""" - if self._use_builtin_server: - self._snapserver_started = asyncio.Event() - self._snapserver_runner = asyncio.create_task(self._builtin_server_runner()) - await asyncio.wait_for(self._snapserver_started.wait(), 10) - - def _handle_disconnect(self, exc: Exception) -> None: - """Handle disconnect callback from snapserver.""" - if self._stop_called: - # we're instructed to stop/exit, so no need to restart the connection - return - self.logger.info( - "Connection to SnapServer lost, reason: %s. Reloading provider in 5 seconds.", - str(exc), - ) - # schedule a reload of the provider - self.mass.call_later(5, self.mass.load_provider, self.instance_id, allow_retry=True) diff --git a/music_assistant/server/providers/snapcast/icon.svg b/music_assistant/server/providers/snapcast/icon.svg deleted file mode 100644 index 853f3659..00000000 --- a/music_assistant/server/providers/snapcast/icon.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/music_assistant/server/providers/snapcast/manifest.json b/music_assistant/server/providers/snapcast/manifest.json deleted file mode 100644 index 0f75f6f2..00000000 --- a/music_assistant/server/providers/snapcast/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "player", - "domain": "snapcast", - "name": "Snapcast", - "description": "Support for snapcast server and clients.", - "codeowners": ["@SantigoSotoC"], - "requirements": ["snapcast==2.3.6", "bidict==0.23.1"], - "documentation": "https://music-assistant.io/player-support/snapcast/", - "multi_instance": false, - "builtin": false -} diff --git a/music_assistant/server/providers/snapcast/snapweb/10-seconds-of-silence.mp3 b/music_assistant/server/providers/snapcast/snapweb/10-seconds-of-silence.mp3 deleted file mode 100644 index 40361eca..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/10-seconds-of-silence.mp3 and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/3rd-party/libflac.js b/music_assistant/server/providers/snapcast/snapweb/3rd-party/libflac.js deleted file mode 100644 index c320c8c0..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/3rd-party/libflac.js +++ /dev/null @@ -1,34568 +0,0 @@ - - -// The Module object: Our interface to the outside world. We import -// and export values on it. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to check if Module already exists (e.g. case 3 above). -// Substitution will be replaced with actual code on later stage of the build, -// this way Closure Compiler will not mangle it (e.g. case 4. above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module = typeof Module !== 'undefined' ? Module : {}; - - - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) -// libflac.js - port of libflac to JavaScript using emscripten - - -(function (root, factory) { - - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['module', 'require'], factory.bind(null, root)); - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - - // use process.env (if available) for reading Flac environment settings: - var env = typeof process !== 'undefined' && process && process.env? process.env : root; - factory(env, module, module.require); - } else { - // Browser globals - root.Flac = factory(root); - } - -}(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this, function (global, expLib, require) { -'use strict'; - -var Module = Module || {}; -var _flac_ready = false; -//in case resources are loaded asynchronously (e.g. *.mem file for minified version): setup "ready" handling -Module["onRuntimeInitialized"] = function(){ - _flac_ready = true; - if(!_exported){ - //if _exported is not yet set (may happen, in case initialization was strictly synchronously), - // do "pause" until sync initialization has run through - setTimeout(function(){do_fire_event('ready', [{type: 'ready', target: _exported}], true);}, 0); - } else { - do_fire_event('ready', [{type: 'ready', target: _exported}], true); - } -}; - -if(global && global.FLAC_SCRIPT_LOCATION){ - - Module["locateFile"] = function(fileName){ - var path = global.FLAC_SCRIPT_LOCATION || ''; - if(path[fileName]){ - return path[fileName]; - } - path += path && !/\/$/.test(path)? '/' : ''; - return path + fileName; - }; - - //NOTE will be overwritten if emscripten has env specific implementation for this - var readBinary = function(filePath){ - - //for Node: use default implementation (copied from generated code): - if(ENVIRONMENT_IS_NODE){ - var ret = read_(filePath, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); - return ret; - } - - //otherwise: try "fallback" to AJAX - return new Promise(function(resolve, reject){ - var xhr = new XMLHttpRequest(); - xhr.responseType = "arraybuffer"; - xhr.addEventListener("load", function(evt){ - resolve(xhr.response); - }); - xhr.addEventListener("error", function(err){ - reject(err); - }); - xhr.open("GET", filePath); - xhr.send(); - }); - }; -} - -//fallback for fetch && support file://-protocol: try read as binary if fetch fails -if(global && typeof global.fetch === 'function'){ - var _fetch = global.fetch; - global.fetch = function(url){ - return _fetch.apply(null, arguments).catch(function(err){ - try{ - var result = readBinary(url); - if(result && result.catch){ - result.catch(function(_err){throw err}); - } - return result; - } catch(_err){ - throw err; - } - }); - }; -} - - - -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = {}; -var key; -for (key in Module) { - if (Module.hasOwnProperty(key)) { - moduleOverrides[key] = Module[key]; - } -} - -var arguments_ = []; -var thisProgram = './this.program'; -var quit_ = function(status, toThrow) { - throw toThrow; -}; - -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -var ENVIRONMENT_IS_WEB = false; -var ENVIRONMENT_IS_WORKER = false; -var ENVIRONMENT_IS_NODE = false; -var ENVIRONMENT_IS_SHELL = false; -ENVIRONMENT_IS_WEB = typeof window === 'object'; -ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'; -ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - - - - -// `/` should be present at the end if `scriptDirectory` is not empty -var scriptDirectory = ''; -function locateFile(path) { - if (Module['locateFile']) { - return Module['locateFile'](path, scriptDirectory); - } - return scriptDirectory + path; -} - -// Hooks that are implemented differently in different runtime environments. -var read_, - readAsync, - readBinary, - setWindowTitle; - -var nodeFS; -var nodePath; - -if (ENVIRONMENT_IS_NODE) { - if (ENVIRONMENT_IS_WORKER) { - scriptDirectory = require('path').dirname(scriptDirectory) + '/'; - } else { - scriptDirectory = __dirname + '/'; - } - - - - - read_ = function shell_read(filename, binary) { - var ret = tryParseAsDataURI(filename); - if (ret) { - return binary ? ret : ret.toString(); - } - if (!nodeFS) nodeFS = require('fs'); - if (!nodePath) nodePath = require('path'); - filename = nodePath['normalize'](filename); - return nodeFS['readFileSync'](filename, binary ? null : 'utf8'); - }; - - readBinary = function readBinary(filename) { - var ret = read_(filename, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); - return ret; - }; - - - - - if (process['argv'].length > 1) { - thisProgram = process['argv'][1].replace(/\\/g, '/'); - } - - arguments_ = process['argv'].slice(2); - - if (typeof module !== 'undefined') { - module['exports'] = Module; - } - - - - quit_ = function(status) { - process['exit'](status); - }; - - Module['inspect'] = function () { return '[Emscripten Module object]'; }; - - - -} else -if (ENVIRONMENT_IS_SHELL) { - - - if (typeof read != 'undefined') { - read_ = function shell_read(f) { - var data = tryParseAsDataURI(f); - if (data) { - return intArrayToString(data); - } - return read(f); - }; - } - - readBinary = function readBinary(f) { - var data; - data = tryParseAsDataURI(f); - if (data) { - return data; - } - if (typeof readbuffer === 'function') { - return new Uint8Array(readbuffer(f)); - } - data = read(f, 'binary'); - assert(typeof data === 'object'); - return data; - }; - - if (typeof scriptArgs != 'undefined') { - arguments_ = scriptArgs; - } else if (typeof arguments != 'undefined') { - arguments_ = arguments; - } - - if (typeof quit === 'function') { - quit_ = function(status) { - quit(status); - }; - } - - if (typeof print !== 'undefined') { - // Prefer to use print/printErr where they exist, as they usually work better. - if (typeof console === 'undefined') console = /** @type{!Console} */({}); - console.log = /** @type{!function(this:Console, ...*): undefined} */ (print); - console.warn = console.error = /** @type{!function(this:Console, ...*): undefined} */ (typeof printErr !== 'undefined' ? printErr : print); - } - - -} else - -// Note that this includes Node.js workers when relevant (pthreads is enabled). -// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and -// ENVIRONMENT_IS_NODE. -if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled - scriptDirectory = self.location.href; - } else if (document.currentScript) { // web - scriptDirectory = document.currentScript.src; - } - // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. - // otherwise, slice off the final part of the url to find the script directory. - // if scriptDirectory does not contain a slash, lastIndexOf will return -1, - // and scriptDirectory will correctly be replaced with an empty string. - if (scriptDirectory.indexOf('blob:') !== 0) { - scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf('/')+1); - } else { - scriptDirectory = ''; - } - - - // Differentiate the Web Worker from the Node Worker case, as reading must - // be done differently. - { - - - - - read_ = function shell_read(url) { - try { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - } catch (err) { - var data = tryParseAsDataURI(url); - if (data) { - return intArrayToString(data); - } - throw err; - } - }; - - if (ENVIRONMENT_IS_WORKER) { - readBinary = function readBinary(url) { - try { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.responseType = 'arraybuffer'; - xhr.send(null); - return new Uint8Array(/** @type{!ArrayBuffer} */(xhr.response)); - } catch (err) { - var data = tryParseAsDataURI(url); - if (data) { - return data; - } - throw err; - } - }; - } - - readAsync = function readAsync(url, onload, onerror) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = function xhr_onload() { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - return; - } - var data = tryParseAsDataURI(url); - if (data) { - onload(data.buffer); - return; - } - onerror(); - }; - xhr.onerror = onerror; - xhr.send(null); - }; - - - - - } - - setWindowTitle = function(title) { document.title = title }; -} else -{ -} - - -// Set up the out() and err() hooks, which are how we can print to stdout or -// stderr, respectively. -var out = Module['print'] || console.log.bind(console); -var err = Module['printErr'] || console.warn.bind(console); - -// Merge back in the overrides -for (key in moduleOverrides) { - if (moduleOverrides.hasOwnProperty(key)) { - Module[key] = moduleOverrides[key]; - } -} -// Free the object hierarchy contained in the overrides, this lets the GC -// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. -moduleOverrides = null; - -// Emit code to handle expected values on the Module object. This applies Module.x -// to the proper local x. This has two benefits: first, we only emit it if it is -// expected to arrive, and second, by using a local everywhere else that can be -// minified. -if (Module['arguments']) arguments_ = Module['arguments']; -if (Module['thisProgram']) thisProgram = Module['thisProgram']; -if (Module['quit']) quit_ = Module['quit']; - -// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message - - - - - -// {{PREAMBLE_ADDITIONS}} - -var STACK_ALIGN = 16; - -function dynamicAlloc(size) { - var ret = HEAP32[DYNAMICTOP_PTR>>2]; - var end = (ret + size + 15) & -16; - HEAP32[DYNAMICTOP_PTR>>2] = end; - return ret; -} - -function alignMemory(size, factor) { - if (!factor) factor = STACK_ALIGN; // stack alignment (16-byte) by default - return Math.ceil(size / factor) * factor; -} - -function getNativeTypeSize(type) { - switch (type) { - case 'i1': case 'i8': return 1; - case 'i16': return 2; - case 'i32': return 4; - case 'i64': return 8; - case 'float': return 4; - case 'double': return 8; - default: { - if (type[type.length-1] === '*') { - return 4; // A pointer - } else if (type[0] === 'i') { - var bits = Number(type.substr(1)); - assert(bits % 8 === 0, 'getNativeTypeSize invalid bits ' + bits + ', type ' + type); - return bits / 8; - } else { - return 0; - } - } - } -} - -function warnOnce(text) { - if (!warnOnce.shown) warnOnce.shown = {}; - if (!warnOnce.shown[text]) { - warnOnce.shown[text] = 1; - err(text); - } -} - - - - - - - - -// Wraps a JS function as a wasm function with a given signature. -function convertJsFunctionToWasm(func, sig) { - return func; -} - -var freeTableIndexes = []; - -// Weak map of functions in the table to their indexes, created on first use. -var functionsInTableMap; - -// Add a wasm function to the table. -function addFunctionWasm(func, sig) { - var table = wasmTable; - - // Check if the function is already in the table, to ensure each function - // gets a unique index. First, create the map if this is the first use. - if (!functionsInTableMap) { - functionsInTableMap = new WeakMap(); - for (var i = 0; i < table.length; i++) { - var item = table.get(i); - // Ignore null values. - if (item) { - functionsInTableMap.set(item, i); - } - } - } - if (functionsInTableMap.has(func)) { - return functionsInTableMap.get(func); - } - - // It's not in the table, add it now. - - - var ret; - // Reuse a free index if there is one, otherwise grow. - if (freeTableIndexes.length) { - ret = freeTableIndexes.pop(); - } else { - ret = table.length; - // Grow the table - try { - table.grow(1); - } catch (err) { - if (!(err instanceof RangeError)) { - throw err; - } - throw 'Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.'; - } - } - - // Set the new value. - try { - // Attempting to call this with JS function will cause of table.set() to fail - table.set(ret, func); - } catch (err) { - if (!(err instanceof TypeError)) { - throw err; - } - var wrapped = convertJsFunctionToWasm(func, sig); - table.set(ret, wrapped); - } - - functionsInTableMap.set(func, ret); - - return ret; -} - -function removeFunctionWasm(index) { - functionsInTableMap.delete(wasmTable.get(index)); - freeTableIndexes.push(index); -} - -// 'sig' parameter is required for the llvm backend but only when func is not -// already a WebAssembly function. -function addFunction(func, sig) { - - return addFunctionWasm(func, sig); -} - -function removeFunction(index) { - removeFunctionWasm(index); -} - - - -var funcWrappers = {}; - -function getFuncWrapper(func, sig) { - if (!func) return; // on null pointer, return undefined - assert(sig); - if (!funcWrappers[sig]) { - funcWrappers[sig] = {}; - } - var sigCache = funcWrappers[sig]; - if (!sigCache[func]) { - // optimize away arguments usage in common cases - if (sig.length === 1) { - sigCache[func] = function dynCall_wrapper() { - return dynCall(sig, func); - }; - } else if (sig.length === 2) { - sigCache[func] = function dynCall_wrapper(arg) { - return dynCall(sig, func, [arg]); - }; - } else { - // general case - sigCache[func] = function dynCall_wrapper() { - return dynCall(sig, func, Array.prototype.slice.call(arguments)); - }; - } - } - return sigCache[func]; -} - - - - - - - -function makeBigInt(low, high, unsigned) { - return unsigned ? ((+((low>>>0)))+((+((high>>>0)))*4294967296.0)) : ((+((low>>>0)))+((+((high|0)))*4294967296.0)); -} - -/** @param {Array=} args */ -function dynCall(sig, ptr, args) { - if (args && args.length) { - return Module['dynCall_' + sig].apply(null, [ptr].concat(args)); - } else { - return Module['dynCall_' + sig].call(null, ptr); - } -} - -var tempRet0 = 0; - -var setTempRet0 = function(value) { - tempRet0 = value; -}; - -var getTempRet0 = function() { - return tempRet0; -}; - - -// The address globals begin at. Very low in memory, for code size and optimization opportunities. -// Above 0 is static memory, starting with globals. -// Then the stack. -// Then 'dynamic' memory for sbrk. -var GLOBAL_BASE = 1024; - - - - - -// === Preamble library stuff === - -// Documentation for the public APIs defined in this file must be updated in: -// site/source/docs/api_reference/preamble.js.rst -// A prebuilt local version of the documentation is available at: -// site/build/text/docs/api_reference/preamble.js.txt -// You can also build docs locally as HTML or other formats in site/ -// An online HTML version (which may be of a different version of Emscripten) -// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html - - -var wasmBinary;if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; -var noExitRuntime;if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; - - - - -// wasm2js.js - enough of a polyfill for the WebAssembly object so that we can load -// wasm2js code that way. - -// Emit "var WebAssembly" if definitely using wasm2js. Otherwise, in MAYBE_WASM2JS -// mode, we can't use a "var" since it would prevent normal wasm from working. -/** @suppress{const} */ -var -WebAssembly = { - // Note that we do not use closure quoting (this['buffer'], etc.) on these - // functions, as they are just meant for internal use. In other words, this is - // not a fully general polyfill. - Memory: function(opts) { - this.buffer = new ArrayBuffer(opts['initial'] * 65536); - this.grow = function(amount) { - var ret = __growWasmMemory(amount); - return ret; - }; - }, - - // Table is not a normal constructor and instead returns the array object. - // That lets us use the length property automatically, which is simpler and - // smaller (but instanceof will not report that an instance of Table is an - // instance of this function). - Table: /** @constructor */ function(opts) { - var ret = new Array(opts['initial']); - ret.grow = function(by) { - if (ret.length >= 22 + 5) { - abort('Unable to grow wasm table. Use a higher value for RESERVED_FUNCTION_POINTERS or set ALLOW_TABLE_GROWTH.') - } - ret.push(null); - }; - ret.set = function(i, func) { - ret[i] = func; - }; - ret.get = function(i) { - return ret[i]; - }; - return ret; - }, - - Module: function(binary) { - // TODO: use the binary and info somehow - right now the wasm2js output is embedded in - // the main JS - }, - - Instance: function(module, info) { - // TODO: use the module and info somehow - right now the wasm2js output is embedded in - // the main JS - // This will be replaced by the actual wasm2js code. - this.exports = ( -function instantiate(asmLibraryArg, wasmMemory, wasmTable) { - - - var scratchBuffer = new ArrayBuffer(8); - var i32ScratchView = new Int32Array(scratchBuffer); - var f32ScratchView = new Float32Array(scratchBuffer); - var f64ScratchView = new Float64Array(scratchBuffer); - - function wasm2js_scratch_load_i32(index) { - return i32ScratchView[index]; - } - - function wasm2js_scratch_store_i32(index, value) { - i32ScratchView[index] = value; - } - - function wasm2js_scratch_load_f64() { - return f64ScratchView[0]; - } - - function wasm2js_scratch_store_f64(value) { - f64ScratchView[0] = value; - } - - function wasm2js_scratch_store_f32(value) { - f32ScratchView[0] = value; - } - -function asmFunc(global, env, buffer) { - var memory = env.memory; - var FUNCTION_TABLE = wasmTable; - var HEAP8 = new global.Int8Array(buffer); - var HEAP16 = new global.Int16Array(buffer); - var HEAP32 = new global.Int32Array(buffer); - var HEAPU8 = new global.Uint8Array(buffer); - var HEAPU16 = new global.Uint16Array(buffer); - var HEAPU32 = new global.Uint32Array(buffer); - var HEAPF32 = new global.Float32Array(buffer); - var HEAPF64 = new global.Float64Array(buffer); - var Math_imul = global.Math.imul; - var Math_fround = global.Math.fround; - var Math_abs = global.Math.abs; - var Math_clz32 = global.Math.clz32; - var Math_min = global.Math.min; - var Math_max = global.Math.max; - var Math_floor = global.Math.floor; - var Math_ceil = global.Math.ceil; - var Math_sqrt = global.Math.sqrt; - var abort = env.abort; - var nan = global.NaN; - var infinity = global.Infinity; - var emscripten_resize_heap = env.emscripten_resize_heap; - var emscripten_memcpy_big = env.emscripten_memcpy_big; - var __wasi_fd_close = env.fd_close; - var __wasi_fd_read = env.fd_read; - var round = env.round; - var __wasi_fd_write = env.fd_write; - var setTempRet0 = env.setTempRet0; - var legalimport$__wasi_fd_seek = env.fd_seek; - var global$0 = 5257216; - var global$1 = 14168; - var __wasm_intrinsics_temp_i64 = 0; - var __wasm_intrinsics_temp_i64$hi = 0; - var i64toi32_i32$HIGH_BITS = 0; - // EMSCRIPTEN_START_FUNCS -; - function __wasm_call_ctors() { - - } - - function __errno_location() { - return 11584; - } - - function sbrk($0) { - var $1 = 0, $2 = 0; - $1 = HEAP32[3544]; - $2 = $0 + 3 & -4; - $0 = $1 + $2 | 0; - label$1 : { - if ($0 >>> 0 <= $1 >>> 0 ? ($2 | 0) >= 1 : 0) { - break label$1 - } - if ($0 >>> 0 > __wasm_memory_size() << 16 >>> 0) { - if (!emscripten_resize_heap($0 | 0)) { - break label$1 - } - } - HEAP32[3544] = $0; - return $1; - } - HEAP32[2896] = 48; - return -1; - } - - function memset($0, $1) { - var $2 = 0, $3 = 0; - label$1 : { - if (!$1) { - break label$1 - } - $2 = $0 + $1 | 0; - HEAP8[$2 + -1 | 0] = 0; - HEAP8[$0 | 0] = 0; - if ($1 >>> 0 < 3) { - break label$1 - } - HEAP8[$2 + -2 | 0] = 0; - HEAP8[$0 + 1 | 0] = 0; - HEAP8[$2 + -3 | 0] = 0; - HEAP8[$0 + 2 | 0] = 0; - if ($1 >>> 0 < 7) { - break label$1 - } - HEAP8[$2 + -4 | 0] = 0; - HEAP8[$0 + 3 | 0] = 0; - if ($1 >>> 0 < 9) { - break label$1 - } - $3 = 0 - $0 & 3; - $2 = $3 + $0 | 0; - HEAP32[$2 >> 2] = 0; - $3 = $1 - $3 & -4; - $1 = $3 + $2 | 0; - HEAP32[$1 + -4 >> 2] = 0; - if ($3 >>> 0 < 9) { - break label$1 - } - HEAP32[$2 + 8 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - HEAP32[$1 + -8 >> 2] = 0; - HEAP32[$1 + -12 >> 2] = 0; - if ($3 >>> 0 < 25) { - break label$1 - } - HEAP32[$2 + 24 >> 2] = 0; - HEAP32[$2 + 20 >> 2] = 0; - HEAP32[$2 + 16 >> 2] = 0; - HEAP32[$2 + 12 >> 2] = 0; - HEAP32[$1 + -16 >> 2] = 0; - HEAP32[$1 + -20 >> 2] = 0; - HEAP32[$1 + -24 >> 2] = 0; - HEAP32[$1 + -28 >> 2] = 0; - $1 = $3; - $3 = $2 & 4 | 24; - $1 = $1 - $3 | 0; - if ($1 >>> 0 < 32) { - break label$1 - } - $2 = $2 + $3 | 0; - while (1) { - HEAP32[$2 + 24 >> 2] = 0; - HEAP32[$2 + 28 >> 2] = 0; - HEAP32[$2 + 16 >> 2] = 0; - HEAP32[$2 + 20 >> 2] = 0; - HEAP32[$2 + 8 >> 2] = 0; - HEAP32[$2 + 12 >> 2] = 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - $2 = $2 + 32 | 0; - $1 = $1 + -32 | 0; - if ($1 >>> 0 > 31) { - continue - } - break; - }; - } - return $0; - } - - function memcpy($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0; - if ($2 >>> 0 >= 512) { - emscripten_memcpy_big($0 | 0, $1 | 0, $2 | 0) | 0; - return $0; - } - $4 = $0 + $2 | 0; - label$2 : { - if (!(($0 ^ $1) & 3)) { - label$4 : { - if (($2 | 0) < 1) { - $2 = $0; - break label$4; - } - if (!($0 & 3)) { - $2 = $0; - break label$4; - } - $2 = $0; - while (1) { - HEAP8[$2 | 0] = HEAPU8[$1 | 0]; - $1 = $1 + 1 | 0; - $2 = $2 + 1 | 0; - if ($2 >>> 0 >= $4 >>> 0) { - break label$4 - } - if ($2 & 3) { - continue - } - break; - }; - } - $3 = $4 & -4; - label$8 : { - if ($3 >>> 0 < 64) { - break label$8 - } - $5 = $3 + -64 | 0; - if ($2 >>> 0 > $5 >>> 0) { - break label$8 - } - while (1) { - HEAP32[$2 >> 2] = HEAP32[$1 >> 2]; - HEAP32[$2 + 4 >> 2] = HEAP32[$1 + 4 >> 2]; - HEAP32[$2 + 8 >> 2] = HEAP32[$1 + 8 >> 2]; - HEAP32[$2 + 12 >> 2] = HEAP32[$1 + 12 >> 2]; - HEAP32[$2 + 16 >> 2] = HEAP32[$1 + 16 >> 2]; - HEAP32[$2 + 20 >> 2] = HEAP32[$1 + 20 >> 2]; - HEAP32[$2 + 24 >> 2] = HEAP32[$1 + 24 >> 2]; - HEAP32[$2 + 28 >> 2] = HEAP32[$1 + 28 >> 2]; - HEAP32[$2 + 32 >> 2] = HEAP32[$1 + 32 >> 2]; - HEAP32[$2 + 36 >> 2] = HEAP32[$1 + 36 >> 2]; - HEAP32[$2 + 40 >> 2] = HEAP32[$1 + 40 >> 2]; - HEAP32[$2 + 44 >> 2] = HEAP32[$1 + 44 >> 2]; - HEAP32[$2 + 48 >> 2] = HEAP32[$1 + 48 >> 2]; - HEAP32[$2 + 52 >> 2] = HEAP32[$1 + 52 >> 2]; - HEAP32[$2 + 56 >> 2] = HEAP32[$1 + 56 >> 2]; - HEAP32[$2 + 60 >> 2] = HEAP32[$1 + 60 >> 2]; - $1 = $1 - -64 | 0; - $2 = $2 - -64 | 0; - if ($2 >>> 0 <= $5 >>> 0) { - continue - } - break; - }; - } - if ($2 >>> 0 >= $3 >>> 0) { - break label$2 - } - while (1) { - HEAP32[$2 >> 2] = HEAP32[$1 >> 2]; - $1 = $1 + 4 | 0; - $2 = $2 + 4 | 0; - if ($2 >>> 0 < $3 >>> 0) { - continue - } - break; - }; - break label$2; - } - if ($4 >>> 0 < 4) { - $2 = $0; - break label$2; - } - $3 = $4 + -4 | 0; - if ($3 >>> 0 < $0 >>> 0) { - $2 = $0; - break label$2; - } - $2 = $0; - while (1) { - HEAP8[$2 | 0] = HEAPU8[$1 | 0]; - HEAP8[$2 + 1 | 0] = HEAPU8[$1 + 1 | 0]; - HEAP8[$2 + 2 | 0] = HEAPU8[$1 + 2 | 0]; - HEAP8[$2 + 3 | 0] = HEAPU8[$1 + 3 | 0]; - $1 = $1 + 4 | 0; - $2 = $2 + 4 | 0; - if ($2 >>> 0 <= $3 >>> 0) { - continue - } - break; - }; - } - if ($2 >>> 0 < $4 >>> 0) { - while (1) { - HEAP8[$2 | 0] = HEAPU8[$1 | 0]; - $1 = $1 + 1 | 0; - $2 = $2 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - } - } - return $0; - } - - function dlmalloc($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $11 = global$0 - 16 | 0; - global$0 = $11; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - label$8 : { - label$9 : { - label$10 : { - label$11 : { - if ($0 >>> 0 <= 244) { - $6 = HEAP32[2897]; - $5 = $0 >>> 0 < 11 ? 16 : $0 + 11 & -8; - $0 = $5 >>> 3 | 0; - $1 = $6 >>> $0 | 0; - if ($1 & 3) { - $2 = $0 + (($1 ^ -1) & 1) | 0; - $5 = $2 << 3; - $1 = HEAP32[$5 + 11636 >> 2]; - $0 = $1 + 8 | 0; - $3 = HEAP32[$1 + 8 >> 2]; - $5 = $5 + 11628 | 0; - label$14 : { - if (($3 | 0) == ($5 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = __wasm_rotl_i32(-2, $2) & $6), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$14; - } - HEAP32[$3 + 12 >> 2] = $5; - HEAP32[$5 + 8 >> 2] = $3; - } - $2 = $2 << 3; - HEAP32[$1 + 4 >> 2] = $2 | 3; - $1 = $1 + $2 | 0; - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; - break label$1; - } - $7 = HEAP32[2899]; - if ($5 >>> 0 <= $7 >>> 0) { - break label$11 - } - if ($1) { - $2 = 2 << $0; - $0 = (0 - $2 | $2) & $1 << $0; - $0 = (0 - $0 & $0) + -1 | 0; - $1 = $0 >>> 12 & 16; - $2 = $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 5 & 8; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 2 & 4; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 2; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 1; - $2 = ($2 | $1) + ($0 >>> $1 | 0) | 0; - $3 = $2 << 3; - $1 = HEAP32[$3 + 11636 >> 2]; - $0 = HEAP32[$1 + 8 >> 2]; - $3 = $3 + 11628 | 0; - label$17 : { - if (($0 | 0) == ($3 | 0)) { - $6 = __wasm_rotl_i32(-2, $2) & $6; - HEAP32[2897] = $6; - break label$17; - } - HEAP32[$0 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $0; - } - $0 = $1 + 8 | 0; - HEAP32[$1 + 4 >> 2] = $5 | 3; - $4 = $1 + $5 | 0; - $2 = $2 << 3; - $3 = $2 - $5 | 0; - HEAP32[$4 + 4 >> 2] = $3 | 1; - HEAP32[$1 + $2 >> 2] = $3; - if ($7) { - $5 = $7 >>> 3 | 0; - $1 = ($5 << 3) + 11628 | 0; - $2 = HEAP32[2902]; - $5 = 1 << $5; - label$20 : { - if (!($5 & $6)) { - HEAP32[2897] = $5 | $6; - $5 = $1; - break label$20; - } - $5 = HEAP32[$1 + 8 >> 2]; - } - HEAP32[$1 + 8 >> 2] = $2; - HEAP32[$5 + 12 >> 2] = $2; - HEAP32[$2 + 12 >> 2] = $1; - HEAP32[$2 + 8 >> 2] = $5; - } - HEAP32[2902] = $4; - HEAP32[2899] = $3; - break label$1; - } - $10 = HEAP32[2898]; - if (!$10) { - break label$11 - } - $0 = ($10 & 0 - $10) + -1 | 0; - $1 = $0 >>> 12 & 16; - $2 = $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 5 & 8; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 2 & 4; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 2; - $2 = $2 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 1; - $1 = HEAP32[(($2 | $1) + ($0 >>> $1 | 0) << 2) + 11892 >> 2]; - $3 = (HEAP32[$1 + 4 >> 2] & -8) - $5 | 0; - $2 = $1; - while (1) { - label$23 : { - $0 = HEAP32[$2 + 16 >> 2]; - if (!$0) { - $0 = HEAP32[$2 + 20 >> 2]; - if (!$0) { - break label$23 - } - } - $4 = (HEAP32[$0 + 4 >> 2] & -8) - $5 | 0; - $2 = $4 >>> 0 < $3 >>> 0; - $3 = $2 ? $4 : $3; - $1 = $2 ? $0 : $1; - $2 = $0; - continue; - } - break; - }; - $9 = HEAP32[$1 + 24 >> 2]; - $4 = HEAP32[$1 + 12 >> 2]; - if (($4 | 0) != ($1 | 0)) { - $0 = HEAP32[$1 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $4; - HEAP32[$4 + 8 >> 2] = $0; - break label$2; - } - $2 = $1 + 20 | 0; - $0 = HEAP32[$2 >> 2]; - if (!$0) { - $0 = HEAP32[$1 + 16 >> 2]; - if (!$0) { - break label$10 - } - $2 = $1 + 16 | 0; - } - while (1) { - $8 = $2; - $4 = $0; - $2 = $0 + 20 | 0; - $0 = HEAP32[$2 >> 2]; - if ($0) { - continue - } - $2 = $4 + 16 | 0; - $0 = HEAP32[$4 + 16 >> 2]; - if ($0) { - continue - } - break; - }; - HEAP32[$8 >> 2] = 0; - break label$2; - } - $5 = -1; - if ($0 >>> 0 > 4294967231) { - break label$11 - } - $0 = $0 + 11 | 0; - $5 = $0 & -8; - $8 = HEAP32[2898]; - if (!$8) { - break label$11 - } - $2 = 0 - $5 | 0; - $0 = $0 >>> 8 | 0; - $7 = 0; - label$29 : { - if (!$0) { - break label$29 - } - $7 = 31; - if ($5 >>> 0 > 16777215) { - break label$29 - } - $3 = $0 + 1048320 >>> 16 & 8; - $1 = $0 << $3; - $0 = $1 + 520192 >>> 16 & 4; - $6 = $1 << $0; - $1 = $6 + 245760 >>> 16 & 2; - $0 = ($6 << $1 >>> 15 | 0) - ($1 | ($0 | $3)) | 0; - $7 = ($0 << 1 | $5 >>> $0 + 21 & 1) + 28 | 0; - } - $3 = HEAP32[($7 << 2) + 11892 >> 2]; - label$30 : { - label$31 : { - label$32 : { - if (!$3) { - $0 = 0; - break label$32; - } - $1 = $5 << (($7 | 0) == 31 ? 0 : 25 - ($7 >>> 1 | 0) | 0); - $0 = 0; - while (1) { - label$35 : { - $6 = (HEAP32[$3 + 4 >> 2] & -8) - $5 | 0; - if ($6 >>> 0 >= $2 >>> 0) { - break label$35 - } - $4 = $3; - $2 = $6; - if ($2) { - break label$35 - } - $2 = 0; - $0 = $3; - break label$31; - } - $6 = HEAP32[$3 + 20 >> 2]; - $3 = HEAP32[(($1 >>> 29 & 4) + $3 | 0) + 16 >> 2]; - $0 = $6 ? (($6 | 0) == ($3 | 0) ? $0 : $6) : $0; - $1 = $1 << (($3 | 0) != 0); - if ($3) { - continue - } - break; - }; - } - if (!($0 | $4)) { - $0 = 2 << $7; - $0 = (0 - $0 | $0) & $8; - if (!$0) { - break label$11 - } - $0 = ($0 & 0 - $0) + -1 | 0; - $1 = $0 >>> 12 & 16; - $3 = $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 5 & 8; - $3 = $3 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 2 & 4; - $3 = $3 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 2; - $3 = $3 | $1; - $0 = $0 >>> $1 | 0; - $1 = $0 >>> 1 & 1; - $0 = HEAP32[(($3 | $1) + ($0 >>> $1 | 0) << 2) + 11892 >> 2]; - } - if (!$0) { - break label$30 - } - } - while (1) { - $3 = (HEAP32[$0 + 4 >> 2] & -8) - $5 | 0; - $1 = $3 >>> 0 < $2 >>> 0; - $2 = $1 ? $3 : $2; - $4 = $1 ? $0 : $4; - $1 = HEAP32[$0 + 16 >> 2]; - if ($1) { - $0 = $1 - } else { - $0 = HEAP32[$0 + 20 >> 2] - } - if ($0) { - continue - } - break; - }; - } - if (!$4 | $2 >>> 0 >= HEAP32[2899] - $5 >>> 0) { - break label$11 - } - $7 = HEAP32[$4 + 24 >> 2]; - $1 = HEAP32[$4 + 12 >> 2]; - if (($4 | 0) != ($1 | 0)) { - $0 = HEAP32[$4 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $1; - HEAP32[$1 + 8 >> 2] = $0; - break label$3; - } - $3 = $4 + 20 | 0; - $0 = HEAP32[$3 >> 2]; - if (!$0) { - $0 = HEAP32[$4 + 16 >> 2]; - if (!$0) { - break label$9 - } - $3 = $4 + 16 | 0; - } - while (1) { - $6 = $3; - $1 = $0; - $3 = $0 + 20 | 0; - $0 = HEAP32[$3 >> 2]; - if ($0) { - continue - } - $3 = $1 + 16 | 0; - $0 = HEAP32[$1 + 16 >> 2]; - if ($0) { - continue - } - break; - }; - HEAP32[$6 >> 2] = 0; - break label$3; - } - $1 = HEAP32[2899]; - if ($1 >>> 0 >= $5 >>> 0) { - $0 = HEAP32[2902]; - $2 = $1 - $5 | 0; - label$45 : { - if ($2 >>> 0 >= 16) { - HEAP32[2899] = $2; - $3 = $0 + $5 | 0; - HEAP32[2902] = $3; - HEAP32[$3 + 4 >> 2] = $2 | 1; - HEAP32[$0 + $1 >> 2] = $2; - HEAP32[$0 + 4 >> 2] = $5 | 3; - break label$45; - } - HEAP32[2902] = 0; - HEAP32[2899] = 0; - HEAP32[$0 + 4 >> 2] = $1 | 3; - $1 = $0 + $1 | 0; - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; - } - $0 = $0 + 8 | 0; - break label$1; - } - $1 = HEAP32[2900]; - if ($1 >>> 0 > $5 >>> 0) { - $1 = $1 - $5 | 0; - HEAP32[2900] = $1; - $0 = HEAP32[2903]; - $2 = $0 + $5 | 0; - HEAP32[2903] = $2; - HEAP32[$2 + 4 >> 2] = $1 | 1; - HEAP32[$0 + 4 >> 2] = $5 | 3; - $0 = $0 + 8 | 0; - break label$1; - } - $0 = 0; - $4 = $5 + 47 | 0; - $3 = $4; - if (HEAP32[3015]) { - $2 = HEAP32[3017] - } else { - HEAP32[3018] = -1; - HEAP32[3019] = -1; - HEAP32[3016] = 4096; - HEAP32[3017] = 4096; - HEAP32[3015] = $11 + 12 & -16 ^ 1431655768; - HEAP32[3020] = 0; - HEAP32[3008] = 0; - $2 = 4096; - } - $6 = $3 + $2 | 0; - $8 = 0 - $2 | 0; - $2 = $6 & $8; - if ($2 >>> 0 <= $5 >>> 0) { - break label$1 - } - $3 = HEAP32[3007]; - if ($3) { - $7 = HEAP32[3005]; - $9 = $7 + $2 | 0; - if ($9 >>> 0 <= $7 >>> 0 | $9 >>> 0 > $3 >>> 0) { - break label$1 - } - } - if (HEAPU8[12032] & 4) { - break label$6 - } - label$51 : { - label$52 : { - $3 = HEAP32[2903]; - if ($3) { - $0 = 12036; - while (1) { - $7 = HEAP32[$0 >> 2]; - if ($7 + HEAP32[$0 + 4 >> 2] >>> 0 > $3 >>> 0 ? $7 >>> 0 <= $3 >>> 0 : 0) { - break label$52 - } - $0 = HEAP32[$0 + 8 >> 2]; - if ($0) { - continue - } - break; - }; - } - $1 = sbrk(0); - if (($1 | 0) == -1) { - break label$7 - } - $6 = $2; - $0 = HEAP32[3016]; - $3 = $0 + -1 | 0; - if ($3 & $1) { - $6 = ($2 - $1 | 0) + ($1 + $3 & 0 - $0) | 0 - } - if ($6 >>> 0 <= $5 >>> 0 | $6 >>> 0 > 2147483646) { - break label$7 - } - $0 = HEAP32[3007]; - if ($0) { - $3 = HEAP32[3005]; - $8 = $3 + $6 | 0; - if ($8 >>> 0 <= $3 >>> 0 | $8 >>> 0 > $0 >>> 0) { - break label$7 - } - } - $0 = sbrk($6); - if (($1 | 0) != ($0 | 0)) { - break label$51 - } - break label$5; - } - $6 = $8 & $6 - $1; - if ($6 >>> 0 > 2147483646) { - break label$7 - } - $1 = sbrk($6); - if (($1 | 0) == (HEAP32[$0 >> 2] + HEAP32[$0 + 4 >> 2] | 0)) { - break label$8 - } - $0 = $1; - } - if (!(($0 | 0) == -1 | $5 + 48 >>> 0 <= $6 >>> 0)) { - $1 = HEAP32[3017]; - $1 = $1 + ($4 - $6 | 0) & 0 - $1; - if ($1 >>> 0 > 2147483646) { - $1 = $0; - break label$5; - } - if ((sbrk($1) | 0) != -1) { - $6 = $1 + $6 | 0; - $1 = $0; - break label$5; - } - sbrk(0 - $6 | 0); - break label$7; - } - $1 = $0; - if (($0 | 0) != -1) { - break label$5 - } - break label$7; - } - $4 = 0; - break label$2; - } - $1 = 0; - break label$3; - } - if (($1 | 0) != -1) { - break label$5 - } - } - HEAP32[3008] = HEAP32[3008] | 4; - } - if ($2 >>> 0 > 2147483646) { - break label$4 - } - $1 = sbrk($2); - $0 = sbrk(0); - if ($1 >>> 0 >= $0 >>> 0 | ($1 | 0) == -1 | ($0 | 0) == -1) { - break label$4 - } - $6 = $0 - $1 | 0; - if ($6 >>> 0 <= $5 + 40 >>> 0) { - break label$4 - } - } - $0 = HEAP32[3005] + $6 | 0; - HEAP32[3005] = $0; - if ($0 >>> 0 > HEAPU32[3006]) { - HEAP32[3006] = $0 - } - label$62 : { - label$63 : { - label$64 : { - $3 = HEAP32[2903]; - if ($3) { - $0 = 12036; - while (1) { - $2 = HEAP32[$0 >> 2]; - $4 = HEAP32[$0 + 4 >> 2]; - if (($2 + $4 | 0) == ($1 | 0)) { - break label$64 - } - $0 = HEAP32[$0 + 8 >> 2]; - if ($0) { - continue - } - break; - }; - break label$63; - } - $0 = HEAP32[2901]; - if (!($1 >>> 0 >= $0 >>> 0 ? $0 : 0)) { - HEAP32[2901] = $1 - } - $0 = 0; - HEAP32[3010] = $6; - HEAP32[3009] = $1; - HEAP32[2905] = -1; - HEAP32[2906] = HEAP32[3015]; - HEAP32[3012] = 0; - while (1) { - $2 = $0 << 3; - $3 = $2 + 11628 | 0; - HEAP32[$2 + 11636 >> 2] = $3; - HEAP32[$2 + 11640 >> 2] = $3; - $0 = $0 + 1 | 0; - if (($0 | 0) != 32) { - continue - } - break; - }; - $0 = $6 + -40 | 0; - $2 = $1 + 8 & 7 ? -8 - $1 & 7 : 0; - $3 = $0 - $2 | 0; - HEAP32[2900] = $3; - $2 = $1 + $2 | 0; - HEAP32[2903] = $2; - HEAP32[$2 + 4 >> 2] = $3 | 1; - HEAP32[($0 + $1 | 0) + 4 >> 2] = 40; - HEAP32[2904] = HEAP32[3019]; - break label$62; - } - if (HEAPU8[$0 + 12 | 0] & 8 | $1 >>> 0 <= $3 >>> 0 | $2 >>> 0 > $3 >>> 0) { - break label$63 - } - HEAP32[$0 + 4 >> 2] = $4 + $6; - $0 = $3 + 8 & 7 ? -8 - $3 & 7 : 0; - $1 = $0 + $3 | 0; - HEAP32[2903] = $1; - $2 = HEAP32[2900] + $6 | 0; - $0 = $2 - $0 | 0; - HEAP32[2900] = $0; - HEAP32[$1 + 4 >> 2] = $0 | 1; - HEAP32[($2 + $3 | 0) + 4 >> 2] = 40; - HEAP32[2904] = HEAP32[3019]; - break label$62; - } - $0 = HEAP32[2901]; - if ($1 >>> 0 < $0 >>> 0) { - HEAP32[2901] = $1; - $0 = 0; - } - $2 = $1 + $6 | 0; - $0 = 12036; - label$70 : { - label$71 : { - label$72 : { - label$73 : { - label$74 : { - label$75 : { - while (1) { - if (($2 | 0) != HEAP32[$0 >> 2]) { - $0 = HEAP32[$0 + 8 >> 2]; - if ($0) { - continue - } - break label$75; - } - break; - }; - if (!(HEAPU8[$0 + 12 | 0] & 8)) { - break label$74 - } - } - $0 = 12036; - while (1) { - $2 = HEAP32[$0 >> 2]; - if ($2 >>> 0 <= $3 >>> 0) { - $4 = $2 + HEAP32[$0 + 4 >> 2] | 0; - if ($4 >>> 0 > $3 >>> 0) { - break label$73 - } - } - $0 = HEAP32[$0 + 8 >> 2]; - continue; - }; - } - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + $6; - $7 = ($1 + 8 & 7 ? -8 - $1 & 7 : 0) + $1 | 0; - HEAP32[$7 + 4 >> 2] = $5 | 3; - $1 = $2 + ($2 + 8 & 7 ? -8 - $2 & 7 : 0) | 0; - $0 = ($1 - $7 | 0) - $5 | 0; - $4 = $5 + $7 | 0; - if (($1 | 0) == ($3 | 0)) { - HEAP32[2903] = $4; - $0 = HEAP32[2900] + $0 | 0; - HEAP32[2900] = $0; - HEAP32[$4 + 4 >> 2] = $0 | 1; - break label$71; - } - if (HEAP32[2902] == ($1 | 0)) { - HEAP32[2902] = $4; - $0 = HEAP32[2899] + $0 | 0; - HEAP32[2899] = $0; - HEAP32[$4 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $4 >> 2] = $0; - break label$71; - } - $2 = HEAP32[$1 + 4 >> 2]; - if (($2 & 3) == 1) { - $9 = $2 & -8; - label$83 : { - if ($2 >>> 0 <= 255) { - $3 = HEAP32[$1 + 8 >> 2]; - $5 = $2 >>> 3 | 0; - $2 = HEAP32[$1 + 12 >> 2]; - if (($2 | 0) == ($3 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $5)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$83; - } - HEAP32[$3 + 12 >> 2] = $2; - HEAP32[$2 + 8 >> 2] = $3; - break label$83; - } - $8 = HEAP32[$1 + 24 >> 2]; - $6 = HEAP32[$1 + 12 >> 2]; - label$86 : { - if (($6 | 0) != ($1 | 0)) { - $2 = HEAP32[$1 + 8 >> 2]; - HEAP32[$2 + 12 >> 2] = $6; - HEAP32[$6 + 8 >> 2] = $2; - break label$86; - } - label$89 : { - $3 = $1 + 20 | 0; - $5 = HEAP32[$3 >> 2]; - if ($5) { - break label$89 - } - $3 = $1 + 16 | 0; - $5 = HEAP32[$3 >> 2]; - if ($5) { - break label$89 - } - $6 = 0; - break label$86; - } - while (1) { - $2 = $3; - $6 = $5; - $3 = $5 + 20 | 0; - $5 = HEAP32[$3 >> 2]; - if ($5) { - continue - } - $3 = $6 + 16 | 0; - $5 = HEAP32[$6 + 16 >> 2]; - if ($5) { - continue - } - break; - }; - HEAP32[$2 >> 2] = 0; - } - if (!$8) { - break label$83 - } - $2 = HEAP32[$1 + 28 >> 2]; - $3 = ($2 << 2) + 11892 | 0; - label$91 : { - if (HEAP32[$3 >> 2] == ($1 | 0)) { - HEAP32[$3 >> 2] = $6; - if ($6) { - break label$91 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$83; - } - HEAP32[$8 + (HEAP32[$8 + 16 >> 2] == ($1 | 0) ? 16 : 20) >> 2] = $6; - if (!$6) { - break label$83 - } - } - HEAP32[$6 + 24 >> 2] = $8; - $2 = HEAP32[$1 + 16 >> 2]; - if ($2) { - HEAP32[$6 + 16 >> 2] = $2; - HEAP32[$2 + 24 >> 2] = $6; - } - $2 = HEAP32[$1 + 20 >> 2]; - if (!$2) { - break label$83 - } - HEAP32[$6 + 20 >> 2] = $2; - HEAP32[$2 + 24 >> 2] = $6; - } - $1 = $1 + $9 | 0; - $0 = $0 + $9 | 0; - } - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] & -2; - HEAP32[$4 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $4 >> 2] = $0; - if ($0 >>> 0 <= 255) { - $1 = $0 >>> 3 | 0; - $0 = ($1 << 3) + 11628 | 0; - $2 = HEAP32[2897]; - $1 = 1 << $1; - label$95 : { - if (!($2 & $1)) { - HEAP32[2897] = $1 | $2; - $1 = $0; - break label$95; - } - $1 = HEAP32[$0 + 8 >> 2]; - } - HEAP32[$0 + 8 >> 2] = $4; - HEAP32[$1 + 12 >> 2] = $4; - HEAP32[$4 + 12 >> 2] = $0; - HEAP32[$4 + 8 >> 2] = $1; - break label$71; - } - $6 = $4; - $1 = $0 >>> 8 | 0; - $2 = 0; - label$97 : { - if (!$1) { - break label$97 - } - $2 = 31; - if ($0 >>> 0 > 16777215) { - break label$97 - } - $3 = $1 + 1048320 >>> 16 & 8; - $2 = $1 << $3; - $1 = $2 + 520192 >>> 16 & 4; - $5 = $2 << $1; - $2 = $5 + 245760 >>> 16 & 2; - $1 = ($5 << $2 >>> 15 | 0) - ($2 | ($1 | $3)) | 0; - $2 = ($1 << 1 | $0 >>> $1 + 21 & 1) + 28 | 0; - } - $1 = $2; - HEAP32[$6 + 28 >> 2] = $1; - HEAP32[$4 + 16 >> 2] = 0; - HEAP32[$4 + 20 >> 2] = 0; - $2 = ($1 << 2) + 11892 | 0; - $3 = HEAP32[2898]; - $5 = 1 << $1; - label$98 : { - if (!($3 & $5)) { - HEAP32[2898] = $3 | $5; - HEAP32[$2 >> 2] = $4; - break label$98; - } - $3 = $0 << (($1 | 0) == 31 ? 0 : 25 - ($1 >>> 1 | 0) | 0); - $1 = HEAP32[$2 >> 2]; - while (1) { - $2 = $1; - if ((HEAP32[$1 + 4 >> 2] & -8) == ($0 | 0)) { - break label$72 - } - $1 = $3 >>> 29 | 0; - $3 = $3 << 1; - $5 = ($2 + ($1 & 4) | 0) + 16 | 0; - $1 = HEAP32[$5 >> 2]; - if ($1) { - continue - } - break; - }; - HEAP32[$5 >> 2] = $4; - } - HEAP32[$4 + 24 >> 2] = $2; - HEAP32[$4 + 12 >> 2] = $4; - HEAP32[$4 + 8 >> 2] = $4; - break label$71; - } - $0 = $6 + -40 | 0; - $2 = $1 + 8 & 7 ? -8 - $1 & 7 : 0; - $8 = $0 - $2 | 0; - HEAP32[2900] = $8; - $2 = $1 + $2 | 0; - HEAP32[2903] = $2; - HEAP32[$2 + 4 >> 2] = $8 | 1; - HEAP32[($0 + $1 | 0) + 4 >> 2] = 40; - HEAP32[2904] = HEAP32[3019]; - $0 = ($4 + ($4 + -39 & 7 ? 39 - $4 & 7 : 0) | 0) + -47 | 0; - $2 = $0 >>> 0 < $3 + 16 >>> 0 ? $3 : $0; - HEAP32[$2 + 4 >> 2] = 27; - $0 = HEAP32[3012]; - HEAP32[$2 + 16 >> 2] = HEAP32[3011]; - HEAP32[$2 + 20 >> 2] = $0; - $0 = HEAP32[3010]; - HEAP32[$2 + 8 >> 2] = HEAP32[3009]; - HEAP32[$2 + 12 >> 2] = $0; - HEAP32[3011] = $2 + 8; - HEAP32[3010] = $6; - HEAP32[3009] = $1; - HEAP32[3012] = 0; - $0 = $2 + 24 | 0; - while (1) { - HEAP32[$0 + 4 >> 2] = 7; - $1 = $0 + 8 | 0; - $0 = $0 + 4 | 0; - if ($4 >>> 0 > $1 >>> 0) { - continue - } - break; - }; - if (($2 | 0) == ($3 | 0)) { - break label$62 - } - HEAP32[$2 + 4 >> 2] = HEAP32[$2 + 4 >> 2] & -2; - $6 = $2 - $3 | 0; - HEAP32[$3 + 4 >> 2] = $6 | 1; - HEAP32[$2 >> 2] = $6; - if ($6 >>> 0 <= 255) { - $1 = $6 >>> 3 | 0; - $0 = ($1 << 3) + 11628 | 0; - $2 = HEAP32[2897]; - $1 = 1 << $1; - label$103 : { - if (!($2 & $1)) { - HEAP32[2897] = $1 | $2; - $1 = $0; - break label$103; - } - $1 = HEAP32[$0 + 8 >> 2]; - } - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$1 + 12 >> 2] = $3; - HEAP32[$3 + 12 >> 2] = $0; - HEAP32[$3 + 8 >> 2] = $1; - break label$62; - } - HEAP32[$3 + 16 >> 2] = 0; - HEAP32[$3 + 20 >> 2] = 0; - $7 = $3; - $0 = $6 >>> 8 | 0; - $1 = 0; - label$105 : { - if (!$0) { - break label$105 - } - $1 = 31; - if ($6 >>> 0 > 16777215) { - break label$105 - } - $2 = $0 + 1048320 >>> 16 & 8; - $1 = $0 << $2; - $0 = $1 + 520192 >>> 16 & 4; - $4 = $1 << $0; - $1 = $4 + 245760 >>> 16 & 2; - $0 = ($4 << $1 >>> 15 | 0) - ($1 | ($0 | $2)) | 0; - $1 = ($0 << 1 | $6 >>> $0 + 21 & 1) + 28 | 0; - } - $0 = $1; - HEAP32[$7 + 28 >> 2] = $0; - $1 = ($0 << 2) + 11892 | 0; - $2 = HEAP32[2898]; - $4 = 1 << $0; - label$106 : { - if (!($2 & $4)) { - HEAP32[2898] = $2 | $4; - HEAP32[$1 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $1; - break label$106; - } - $0 = $6 << (($0 | 0) == 31 ? 0 : 25 - ($0 >>> 1 | 0) | 0); - $1 = HEAP32[$1 >> 2]; - while (1) { - $2 = $1; - if (($6 | 0) == (HEAP32[$1 + 4 >> 2] & -8)) { - break label$70 - } - $1 = $0 >>> 29 | 0; - $0 = $0 << 1; - $4 = ($2 + ($1 & 4) | 0) + 16 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - continue - } - break; - }; - HEAP32[$4 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $2; - } - HEAP32[$3 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $3; - break label$62; - } - $0 = HEAP32[$2 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $4; - HEAP32[$2 + 8 >> 2] = $4; - HEAP32[$4 + 24 >> 2] = 0; - HEAP32[$4 + 12 >> 2] = $2; - HEAP32[$4 + 8 >> 2] = $0; - } - $0 = $7 + 8 | 0; - break label$1; - } - $0 = HEAP32[$2 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $3; - HEAP32[$2 + 8 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = 0; - HEAP32[$3 + 12 >> 2] = $2; - HEAP32[$3 + 8 >> 2] = $0; - } - $0 = HEAP32[2900]; - if ($0 >>> 0 <= $5 >>> 0) { - break label$4 - } - $1 = $0 - $5 | 0; - HEAP32[2900] = $1; - $0 = HEAP32[2903]; - $2 = $0 + $5 | 0; - HEAP32[2903] = $2; - HEAP32[$2 + 4 >> 2] = $1 | 1; - HEAP32[$0 + 4 >> 2] = $5 | 3; - $0 = $0 + 8 | 0; - break label$1; - } - HEAP32[2896] = 48; - $0 = 0; - break label$1; - } - label$109 : { - if (!$7) { - break label$109 - } - $0 = HEAP32[$4 + 28 >> 2]; - $3 = ($0 << 2) + 11892 | 0; - label$110 : { - if (HEAP32[$3 >> 2] == ($4 | 0)) { - HEAP32[$3 >> 2] = $1; - if ($1) { - break label$110 - } - $8 = __wasm_rotl_i32(-2, $0) & $8; - HEAP32[2898] = $8; - break label$109; - } - HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($4 | 0) ? 16 : 20) >> 2] = $1; - if (!$1) { - break label$109 - } - } - HEAP32[$1 + 24 >> 2] = $7; - $0 = HEAP32[$4 + 16 >> 2]; - if ($0) { - HEAP32[$1 + 16 >> 2] = $0; - HEAP32[$0 + 24 >> 2] = $1; - } - $0 = HEAP32[$4 + 20 >> 2]; - if (!$0) { - break label$109 - } - HEAP32[$1 + 20 >> 2] = $0; - HEAP32[$0 + 24 >> 2] = $1; - } - label$113 : { - if ($2 >>> 0 <= 15) { - $0 = $2 + $5 | 0; - HEAP32[$4 + 4 >> 2] = $0 | 3; - $0 = $0 + $4 | 0; - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] | 1; - break label$113; - } - HEAP32[$4 + 4 >> 2] = $5 | 3; - $1 = $4 + $5 | 0; - HEAP32[$1 + 4 >> 2] = $2 | 1; - HEAP32[$1 + $2 >> 2] = $2; - if ($2 >>> 0 <= 255) { - $2 = $2 >>> 3 | 0; - $0 = ($2 << 3) + 11628 | 0; - $3 = HEAP32[2897]; - $2 = 1 << $2; - label$116 : { - if (!($3 & $2)) { - HEAP32[2897] = $2 | $3; - $2 = $0; - break label$116; - } - $2 = HEAP32[$0 + 8 >> 2]; - } - HEAP32[$0 + 8 >> 2] = $1; - HEAP32[$2 + 12 >> 2] = $1; - HEAP32[$1 + 12 >> 2] = $0; - HEAP32[$1 + 8 >> 2] = $2; - break label$113; - } - $7 = $1; - $0 = $2 >>> 8 | 0; - $3 = 0; - label$118 : { - if (!$0) { - break label$118 - } - $3 = 31; - if ($2 >>> 0 > 16777215) { - break label$118 - } - $5 = $0 + 1048320 >>> 16 & 8; - $3 = $0 << $5; - $0 = $3 + 520192 >>> 16 & 4; - $6 = $3 << $0; - $3 = $6 + 245760 >>> 16 & 2; - $0 = ($6 << $3 >>> 15 | 0) - ($3 | ($0 | $5)) | 0; - $3 = ($0 << 1 | $2 >>> $0 + 21 & 1) + 28 | 0; - } - $0 = $3; - HEAP32[$7 + 28 >> 2] = $0; - HEAP32[$1 + 16 >> 2] = 0; - HEAP32[$1 + 20 >> 2] = 0; - $3 = ($0 << 2) + 11892 | 0; - label$119 : { - $5 = 1 << $0; - label$120 : { - if (!($5 & $8)) { - HEAP32[2898] = $5 | $8; - HEAP32[$3 >> 2] = $1; - break label$120; - } - $0 = $2 << (($0 | 0) == 31 ? 0 : 25 - ($0 >>> 1 | 0) | 0); - $5 = HEAP32[$3 >> 2]; - while (1) { - $3 = $5; - if ((HEAP32[$3 + 4 >> 2] & -8) == ($2 | 0)) { - break label$119 - } - $5 = $0 >>> 29 | 0; - $0 = $0 << 1; - $6 = ($3 + ($5 & 4) | 0) + 16 | 0; - $5 = HEAP32[$6 >> 2]; - if ($5) { - continue - } - break; - }; - HEAP32[$6 >> 2] = $1; - } - HEAP32[$1 + 24 >> 2] = $3; - HEAP32[$1 + 12 >> 2] = $1; - HEAP32[$1 + 8 >> 2] = $1; - break label$113; - } - $0 = HEAP32[$3 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $1; - HEAP32[$3 + 8 >> 2] = $1; - HEAP32[$1 + 24 >> 2] = 0; - HEAP32[$1 + 12 >> 2] = $3; - HEAP32[$1 + 8 >> 2] = $0; - } - $0 = $4 + 8 | 0; - break label$1; - } - label$123 : { - if (!$9) { - break label$123 - } - $0 = HEAP32[$1 + 28 >> 2]; - $2 = ($0 << 2) + 11892 | 0; - label$124 : { - if (HEAP32[$2 >> 2] == ($1 | 0)) { - HEAP32[$2 >> 2] = $4; - if ($4) { - break label$124 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = __wasm_rotl_i32(-2, $0) & $10), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$123; - } - HEAP32[(HEAP32[$9 + 16 >> 2] == ($1 | 0) ? 16 : 20) + $9 >> 2] = $4; - if (!$4) { - break label$123 - } - } - HEAP32[$4 + 24 >> 2] = $9; - $0 = HEAP32[$1 + 16 >> 2]; - if ($0) { - HEAP32[$4 + 16 >> 2] = $0; - HEAP32[$0 + 24 >> 2] = $4; - } - $0 = HEAP32[$1 + 20 >> 2]; - if (!$0) { - break label$123 - } - HEAP32[$4 + 20 >> 2] = $0; - HEAP32[$0 + 24 >> 2] = $4; - } - label$127 : { - if ($3 >>> 0 <= 15) { - $0 = $3 + $5 | 0; - HEAP32[$1 + 4 >> 2] = $0 | 3; - $0 = $0 + $1 | 0; - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] | 1; - break label$127; - } - HEAP32[$1 + 4 >> 2] = $5 | 3; - $5 = $1 + $5 | 0; - HEAP32[$5 + 4 >> 2] = $3 | 1; - HEAP32[$3 + $5 >> 2] = $3; - if ($7) { - $4 = $7 >>> 3 | 0; - $0 = ($4 << 3) + 11628 | 0; - $2 = HEAP32[2902]; - $4 = 1 << $4; - label$130 : { - if (!($4 & $6)) { - HEAP32[2897] = $4 | $6; - $6 = $0; - break label$130; - } - $6 = HEAP32[$0 + 8 >> 2]; - } - HEAP32[$0 + 8 >> 2] = $2; - HEAP32[$6 + 12 >> 2] = $2; - HEAP32[$2 + 12 >> 2] = $0; - HEAP32[$2 + 8 >> 2] = $6; - } - HEAP32[2902] = $5; - HEAP32[2899] = $3; - } - $0 = $1 + 8 | 0; - } - global$0 = $11 + 16 | 0; - return $0 | 0; - } - - function dlfree($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - label$1 : { - if (!$0) { - break label$1 - } - $3 = $0 + -8 | 0; - $2 = HEAP32[$0 + -4 >> 2]; - $0 = $2 & -8; - $5 = $3 + $0 | 0; - label$2 : { - if ($2 & 1) { - break label$2 - } - if (!($2 & 3)) { - break label$1 - } - $2 = HEAP32[$3 >> 2]; - $3 = $3 - $2 | 0; - if ($3 >>> 0 < HEAPU32[2901]) { - break label$1 - } - $0 = $0 + $2 | 0; - if (HEAP32[2902] != ($3 | 0)) { - if ($2 >>> 0 <= 255) { - $4 = HEAP32[$3 + 8 >> 2]; - $2 = $2 >>> 3 | 0; - $1 = HEAP32[$3 + 12 >> 2]; - if (($1 | 0) == ($4 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$2; - } - HEAP32[$4 + 12 >> 2] = $1; - HEAP32[$1 + 8 >> 2] = $4; - break label$2; - } - $7 = HEAP32[$3 + 24 >> 2]; - $2 = HEAP32[$3 + 12 >> 2]; - label$6 : { - if (($2 | 0) != ($3 | 0)) { - $1 = HEAP32[$3 + 8 >> 2]; - HEAP32[$1 + 12 >> 2] = $2; - HEAP32[$2 + 8 >> 2] = $1; - break label$6; - } - label$9 : { - $4 = $3 + 20 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - break label$9 - } - $4 = $3 + 16 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - break label$9 - } - $2 = 0; - break label$6; - } - while (1) { - $6 = $4; - $2 = $1; - $4 = $2 + 20 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - continue - } - $4 = $2 + 16 | 0; - $1 = HEAP32[$2 + 16 >> 2]; - if ($1) { - continue - } - break; - }; - HEAP32[$6 >> 2] = 0; - } - if (!$7) { - break label$2 - } - $4 = HEAP32[$3 + 28 >> 2]; - $1 = ($4 << 2) + 11892 | 0; - label$11 : { - if (HEAP32[$1 >> 2] == ($3 | 0)) { - HEAP32[$1 >> 2] = $2; - if ($2) { - break label$11 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$2; - } - HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($3 | 0) ? 16 : 20) >> 2] = $2; - if (!$2) { - break label$2 - } - } - HEAP32[$2 + 24 >> 2] = $7; - $1 = HEAP32[$3 + 16 >> 2]; - if ($1) { - HEAP32[$2 + 16 >> 2] = $1; - HEAP32[$1 + 24 >> 2] = $2; - } - $1 = HEAP32[$3 + 20 >> 2]; - if (!$1) { - break label$2 - } - HEAP32[$2 + 20 >> 2] = $1; - HEAP32[$1 + 24 >> 2] = $2; - break label$2; - } - $2 = HEAP32[$5 + 4 >> 2]; - if (($2 & 3) != 3) { - break label$2 - } - HEAP32[2899] = $0; - HEAP32[$5 + 4 >> 2] = $2 & -2; - HEAP32[$3 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $3 >> 2] = $0; - return; - } - if ($5 >>> 0 <= $3 >>> 0) { - break label$1 - } - $2 = HEAP32[$5 + 4 >> 2]; - if (!($2 & 1)) { - break label$1 - } - label$14 : { - if (!($2 & 2)) { - if (($5 | 0) == HEAP32[2903]) { - HEAP32[2903] = $3; - $0 = HEAP32[2900] + $0 | 0; - HEAP32[2900] = $0; - HEAP32[$3 + 4 >> 2] = $0 | 1; - if (HEAP32[2902] != ($3 | 0)) { - break label$1 - } - HEAP32[2899] = 0; - HEAP32[2902] = 0; - return; - } - if (($5 | 0) == HEAP32[2902]) { - HEAP32[2902] = $3; - $0 = HEAP32[2899] + $0 | 0; - HEAP32[2899] = $0; - HEAP32[$3 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $3 >> 2] = $0; - return; - } - $0 = ($2 & -8) + $0 | 0; - label$18 : { - if ($2 >>> 0 <= 255) { - $1 = HEAP32[$5 + 8 >> 2]; - $2 = $2 >>> 3 | 0; - $4 = HEAP32[$5 + 12 >> 2]; - if (($1 | 0) == ($4 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$18; - } - HEAP32[$1 + 12 >> 2] = $4; - HEAP32[$4 + 8 >> 2] = $1; - break label$18; - } - $7 = HEAP32[$5 + 24 >> 2]; - $2 = HEAP32[$5 + 12 >> 2]; - label$23 : { - if (($5 | 0) != ($2 | 0)) { - $1 = HEAP32[$5 + 8 >> 2]; - HEAP32[$1 + 12 >> 2] = $2; - HEAP32[$2 + 8 >> 2] = $1; - break label$23; - } - label$26 : { - $4 = $5 + 20 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - break label$26 - } - $4 = $5 + 16 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - break label$26 - } - $2 = 0; - break label$23; - } - while (1) { - $6 = $4; - $2 = $1; - $4 = $2 + 20 | 0; - $1 = HEAP32[$4 >> 2]; - if ($1) { - continue - } - $4 = $2 + 16 | 0; - $1 = HEAP32[$2 + 16 >> 2]; - if ($1) { - continue - } - break; - }; - HEAP32[$6 >> 2] = 0; - } - if (!$7) { - break label$18 - } - $4 = HEAP32[$5 + 28 >> 2]; - $1 = ($4 << 2) + 11892 | 0; - label$28 : { - if (($5 | 0) == HEAP32[$1 >> 2]) { - HEAP32[$1 >> 2] = $2; - if ($2) { - break label$28 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$18; - } - HEAP32[$7 + (($5 | 0) == HEAP32[$7 + 16 >> 2] ? 16 : 20) >> 2] = $2; - if (!$2) { - break label$18 - } - } - HEAP32[$2 + 24 >> 2] = $7; - $1 = HEAP32[$5 + 16 >> 2]; - if ($1) { - HEAP32[$2 + 16 >> 2] = $1; - HEAP32[$1 + 24 >> 2] = $2; - } - $1 = HEAP32[$5 + 20 >> 2]; - if (!$1) { - break label$18 - } - HEAP32[$2 + 20 >> 2] = $1; - HEAP32[$1 + 24 >> 2] = $2; - } - HEAP32[$3 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $3 >> 2] = $0; - if (HEAP32[2902] != ($3 | 0)) { - break label$14 - } - HEAP32[2899] = $0; - return; - } - HEAP32[$5 + 4 >> 2] = $2 & -2; - HEAP32[$3 + 4 >> 2] = $0 | 1; - HEAP32[$0 + $3 >> 2] = $0; - } - if ($0 >>> 0 <= 255) { - $0 = $0 >>> 3 | 0; - $2 = ($0 << 3) + 11628 | 0; - $1 = HEAP32[2897]; - $0 = 1 << $0; - label$32 : { - if (!($1 & $0)) { - HEAP32[2897] = $0 | $1; - $0 = $2; - break label$32; - } - $0 = HEAP32[$2 + 8 >> 2]; - } - HEAP32[$2 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $3; - HEAP32[$3 + 12 >> 2] = $2; - HEAP32[$3 + 8 >> 2] = $0; - return; - } - HEAP32[$3 + 16 >> 2] = 0; - HEAP32[$3 + 20 >> 2] = 0; - $5 = $3; - $4 = $0 >>> 8 | 0; - $1 = 0; - label$34 : { - if (!$4) { - break label$34 - } - $1 = 31; - if ($0 >>> 0 > 16777215) { - break label$34 - } - $2 = $4; - $4 = $4 + 1048320 >>> 16 & 8; - $1 = $2 << $4; - $7 = $1 + 520192 >>> 16 & 4; - $1 = $1 << $7; - $6 = $1 + 245760 >>> 16 & 2; - $1 = ($1 << $6 >>> 15 | 0) - ($6 | ($4 | $7)) | 0; - $1 = ($1 << 1 | $0 >>> $1 + 21 & 1) + 28 | 0; - } - HEAP32[$5 + 28 >> 2] = $1; - $6 = ($1 << 2) + 11892 | 0; - label$35 : { - label$36 : { - $4 = HEAP32[2898]; - $2 = 1 << $1; - label$37 : { - if (!($4 & $2)) { - HEAP32[2898] = $2 | $4; - HEAP32[$6 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $6; - break label$37; - } - $4 = $0 << (($1 | 0) == 31 ? 0 : 25 - ($1 >>> 1 | 0) | 0); - $2 = HEAP32[$6 >> 2]; - while (1) { - $1 = $2; - if ((HEAP32[$2 + 4 >> 2] & -8) == ($0 | 0)) { - break label$36 - } - $2 = $4 >>> 29 | 0; - $4 = $4 << 1; - $6 = ($1 + ($2 & 4) | 0) + 16 | 0; - $2 = HEAP32[$6 >> 2]; - if ($2) { - continue - } - break; - }; - HEAP32[$6 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $1; - } - HEAP32[$3 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $3; - break label$35; - } - $0 = HEAP32[$1 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $3; - HEAP32[$1 + 8 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = 0; - HEAP32[$3 + 12 >> 2] = $1; - HEAP32[$3 + 8 >> 2] = $0; - } - $0 = HEAP32[2905] + -1 | 0; - HEAP32[2905] = $0; - if ($0) { - break label$1 - } - $3 = 12044; - while (1) { - $0 = HEAP32[$3 >> 2]; - $3 = $0 + 8 | 0; - if ($0) { - continue - } - break; - }; - HEAP32[2905] = -1; - } - } - - function dlcalloc($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $2 = 0; - label$2 : { - if (!$0) { - break label$2 - } - $3 = __wasm_i64_mul($0, 0, $1, 0); - $4 = i64toi32_i32$HIGH_BITS; - $2 = $3; - if (($0 | $1) >>> 0 < 65536) { - break label$2 - } - $2 = $4 ? -1 : $3; - } - $1 = $2; - $0 = dlmalloc($1); - if (!(!$0 | !(HEAPU8[$0 + -4 | 0] & 3))) { - memset($0, $1) - } - return $0; - } - - function dlrealloc($0, $1) { - var $2 = 0, $3 = 0; - if (!$0) { - return dlmalloc($1) - } - if ($1 >>> 0 >= 4294967232) { - HEAP32[2896] = 48; - return 0; - } - $2 = try_realloc_chunk($0 + -8 | 0, $1 >>> 0 < 11 ? 16 : $1 + 11 & -8); - if ($2) { - return $2 + 8 | 0 - } - $2 = dlmalloc($1); - if (!$2) { - return 0 - } - $3 = HEAP32[$0 + -4 >> 2]; - $3 = ($3 & 3 ? -4 : -8) + ($3 & -8) | 0; - memcpy($2, $0, $3 >>> 0 < $1 >>> 0 ? $3 : $1); - dlfree($0); - return $2; - } - - function try_realloc_chunk($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $7 = HEAP32[$0 + 4 >> 2]; - $2 = $7 & 3; - $3 = $7 & -8; - $5 = $3 + $0 | 0; - label$2 : { - if (!$2) { - $2 = 0; - if ($1 >>> 0 < 256) { - break label$2 - } - if ($3 >>> 0 >= $1 + 4 >>> 0) { - $2 = $0; - if ($3 - $1 >>> 0 <= HEAP32[3017] << 1 >>> 0) { - break label$2 - } - } - return 0; - } - label$5 : { - if ($3 >>> 0 >= $1 >>> 0) { - $2 = $3 - $1 | 0; - if ($2 >>> 0 < 16) { - break label$5 - } - HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; - $1 = $0 + $1 | 0; - HEAP32[$1 + 4 >> 2] = $2 | 3; - HEAP32[$5 + 4 >> 2] = HEAP32[$5 + 4 >> 2] | 1; - dispose_chunk($1, $2); - break label$5; - } - $2 = 0; - if (($5 | 0) == HEAP32[2903]) { - $4 = $3 + HEAP32[2900] | 0; - if ($4 >>> 0 <= $1 >>> 0) { - break label$2 - } - HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; - $2 = $0 + $1 | 0; - $1 = $4 - $1 | 0; - HEAP32[$2 + 4 >> 2] = $1 | 1; - HEAP32[2900] = $1; - HEAP32[2903] = $2; - break label$5; - } - if (($5 | 0) == HEAP32[2902]) { - $4 = $3 + HEAP32[2899] | 0; - if ($4 >>> 0 < $1 >>> 0) { - break label$2 - } - $2 = $4 - $1 | 0; - label$9 : { - if ($2 >>> 0 >= 16) { - HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; - $1 = $0 + $1 | 0; - HEAP32[$1 + 4 >> 2] = $2 | 1; - $4 = $0 + $4 | 0; - HEAP32[$4 >> 2] = $2; - HEAP32[$4 + 4 >> 2] = HEAP32[$4 + 4 >> 2] & -2; - break label$9; - } - HEAP32[$0 + 4 >> 2] = $4 | $7 & 1 | 2; - $1 = $0 + $4 | 0; - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; - $2 = 0; - $1 = 0; - } - HEAP32[2902] = $1; - HEAP32[2899] = $2; - break label$5; - } - $6 = HEAP32[$5 + 4 >> 2]; - if ($6 & 2) { - break label$2 - } - $8 = $3 + ($6 & -8) | 0; - if ($8 >>> 0 < $1 >>> 0) { - break label$2 - } - $10 = $8 - $1 | 0; - label$11 : { - if ($6 >>> 0 <= 255) { - $2 = $6 >>> 3 | 0; - $6 = HEAP32[$5 + 8 >> 2]; - $4 = HEAP32[$5 + 12 >> 2]; - if (($6 | 0) == ($4 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $2)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$11; - } - HEAP32[$6 + 12 >> 2] = $4; - HEAP32[$4 + 8 >> 2] = $6; - break label$11; - } - $9 = HEAP32[$5 + 24 >> 2]; - $3 = HEAP32[$5 + 12 >> 2]; - label$14 : { - if (($5 | 0) != ($3 | 0)) { - $2 = HEAP32[$5 + 8 >> 2]; - HEAP32[$2 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $2; - break label$14; - } - label$17 : { - $2 = $5 + 20 | 0; - $6 = HEAP32[$2 >> 2]; - if ($6) { - break label$17 - } - $2 = $5 + 16 | 0; - $6 = HEAP32[$2 >> 2]; - if ($6) { - break label$17 - } - $3 = 0; - break label$14; - } - while (1) { - $4 = $2; - $3 = $6; - $2 = $3 + 20 | 0; - $6 = HEAP32[$2 >> 2]; - if ($6) { - continue - } - $2 = $3 + 16 | 0; - $6 = HEAP32[$3 + 16 >> 2]; - if ($6) { - continue - } - break; - }; - HEAP32[$4 >> 2] = 0; - } - if (!$9) { - break label$11 - } - $4 = HEAP32[$5 + 28 >> 2]; - $2 = ($4 << 2) + 11892 | 0; - label$19 : { - if (($5 | 0) == HEAP32[$2 >> 2]) { - HEAP32[$2 >> 2] = $3; - if ($3) { - break label$19 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$11; - } - HEAP32[(($5 | 0) == HEAP32[$9 + 16 >> 2] ? 16 : 20) + $9 >> 2] = $3; - if (!$3) { - break label$11 - } - } - HEAP32[$3 + 24 >> 2] = $9; - $2 = HEAP32[$5 + 16 >> 2]; - if ($2) { - HEAP32[$3 + 16 >> 2] = $2; - HEAP32[$2 + 24 >> 2] = $3; - } - $2 = HEAP32[$5 + 20 >> 2]; - if (!$2) { - break label$11 - } - HEAP32[$3 + 20 >> 2] = $2; - HEAP32[$2 + 24 >> 2] = $3; - } - if ($10 >>> 0 <= 15) { - HEAP32[$0 + 4 >> 2] = $7 & 1 | $8 | 2; - $1 = $0 + $8 | 0; - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; - break label$5; - } - HEAP32[$0 + 4 >> 2] = $7 & 1 | $1 | 2; - $2 = $0 + $1 | 0; - HEAP32[$2 + 4 >> 2] = $10 | 3; - $1 = $0 + $8 | 0; - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] | 1; - dispose_chunk($2, $10); - } - $2 = $0; - } - return $2; - } - - function dispose_chunk($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $5 = $0 + $1 | 0; - label$1 : { - label$2 : { - $2 = HEAP32[$0 + 4 >> 2]; - if ($2 & 1) { - break label$2 - } - if (!($2 & 3)) { - break label$1 - } - $2 = HEAP32[$0 >> 2]; - $1 = $2 + $1 | 0; - $0 = $0 - $2 | 0; - if (($0 | 0) != HEAP32[2902]) { - if ($2 >>> 0 <= 255) { - $4 = $2 >>> 3 | 0; - $2 = HEAP32[$0 + 8 >> 2]; - $3 = HEAP32[$0 + 12 >> 2]; - if (($3 | 0) == ($2 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$2; - } - HEAP32[$2 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $2; - break label$2; - } - $7 = HEAP32[$0 + 24 >> 2]; - $2 = HEAP32[$0 + 12 >> 2]; - label$6 : { - if (($2 | 0) != ($0 | 0)) { - $3 = HEAP32[$0 + 8 >> 2]; - HEAP32[$3 + 12 >> 2] = $2; - HEAP32[$2 + 8 >> 2] = $3; - break label$6; - } - label$9 : { - $3 = $0 + 20 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - break label$9 - } - $3 = $0 + 16 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - break label$9 - } - $2 = 0; - break label$6; - } - while (1) { - $6 = $3; - $2 = $4; - $3 = $2 + 20 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - continue - } - $3 = $2 + 16 | 0; - $4 = HEAP32[$2 + 16 >> 2]; - if ($4) { - continue - } - break; - }; - HEAP32[$6 >> 2] = 0; - } - if (!$7) { - break label$2 - } - $3 = HEAP32[$0 + 28 >> 2]; - $4 = ($3 << 2) + 11892 | 0; - label$11 : { - if (HEAP32[$4 >> 2] == ($0 | 0)) { - HEAP32[$4 >> 2] = $2; - if ($2) { - break label$11 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $3)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$2; - } - HEAP32[$7 + (HEAP32[$7 + 16 >> 2] == ($0 | 0) ? 16 : 20) >> 2] = $2; - if (!$2) { - break label$2 - } - } - HEAP32[$2 + 24 >> 2] = $7; - $3 = HEAP32[$0 + 16 >> 2]; - if ($3) { - HEAP32[$2 + 16 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $2; - } - $3 = HEAP32[$0 + 20 >> 2]; - if (!$3) { - break label$2 - } - HEAP32[$2 + 20 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $2; - break label$2; - } - $2 = HEAP32[$5 + 4 >> 2]; - if (($2 & 3) != 3) { - break label$2 - } - HEAP32[2899] = $1; - HEAP32[$5 + 4 >> 2] = $2 & -2; - HEAP32[$0 + 4 >> 2] = $1 | 1; - HEAP32[$5 >> 2] = $1; - return; - } - $2 = HEAP32[$5 + 4 >> 2]; - label$14 : { - if (!($2 & 2)) { - if (($5 | 0) == HEAP32[2903]) { - HEAP32[2903] = $0; - $1 = HEAP32[2900] + $1 | 0; - HEAP32[2900] = $1; - HEAP32[$0 + 4 >> 2] = $1 | 1; - if (HEAP32[2902] != ($0 | 0)) { - break label$1 - } - HEAP32[2899] = 0; - HEAP32[2902] = 0; - return; - } - if (($5 | 0) == HEAP32[2902]) { - HEAP32[2902] = $0; - $1 = HEAP32[2899] + $1 | 0; - HEAP32[2899] = $1; - HEAP32[$0 + 4 >> 2] = $1 | 1; - HEAP32[$0 + $1 >> 2] = $1; - return; - } - $1 = ($2 & -8) + $1 | 0; - label$18 : { - if ($2 >>> 0 <= 255) { - $4 = $2 >>> 3 | 0; - $2 = HEAP32[$5 + 8 >> 2]; - $3 = HEAP32[$5 + 12 >> 2]; - if (($2 | 0) == ($3 | 0)) { - (wasm2js_i32$0 = 11588, wasm2js_i32$1 = HEAP32[2897] & __wasm_rotl_i32(-2, $4)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$18; - } - HEAP32[$2 + 12 >> 2] = $3; - HEAP32[$3 + 8 >> 2] = $2; - break label$18; - } - $7 = HEAP32[$5 + 24 >> 2]; - $2 = HEAP32[$5 + 12 >> 2]; - label$21 : { - if (($5 | 0) != ($2 | 0)) { - $3 = HEAP32[$5 + 8 >> 2]; - HEAP32[$3 + 12 >> 2] = $2; - HEAP32[$2 + 8 >> 2] = $3; - break label$21; - } - label$24 : { - $3 = $5 + 20 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - break label$24 - } - $3 = $5 + 16 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - break label$24 - } - $2 = 0; - break label$21; - } - while (1) { - $6 = $3; - $2 = $4; - $3 = $2 + 20 | 0; - $4 = HEAP32[$3 >> 2]; - if ($4) { - continue - } - $3 = $2 + 16 | 0; - $4 = HEAP32[$2 + 16 >> 2]; - if ($4) { - continue - } - break; - }; - HEAP32[$6 >> 2] = 0; - } - if (!$7) { - break label$18 - } - $3 = HEAP32[$5 + 28 >> 2]; - $4 = ($3 << 2) + 11892 | 0; - label$26 : { - if (($5 | 0) == HEAP32[$4 >> 2]) { - HEAP32[$4 >> 2] = $2; - if ($2) { - break label$26 - } - (wasm2js_i32$0 = 11592, wasm2js_i32$1 = HEAP32[2898] & __wasm_rotl_i32(-2, $3)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - break label$18; - } - HEAP32[$7 + (($5 | 0) == HEAP32[$7 + 16 >> 2] ? 16 : 20) >> 2] = $2; - if (!$2) { - break label$18 - } - } - HEAP32[$2 + 24 >> 2] = $7; - $3 = HEAP32[$5 + 16 >> 2]; - if ($3) { - HEAP32[$2 + 16 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $2; - } - $3 = HEAP32[$5 + 20 >> 2]; - if (!$3) { - break label$18 - } - HEAP32[$2 + 20 >> 2] = $3; - HEAP32[$3 + 24 >> 2] = $2; - } - HEAP32[$0 + 4 >> 2] = $1 | 1; - HEAP32[$0 + $1 >> 2] = $1; - if (HEAP32[2902] != ($0 | 0)) { - break label$14 - } - HEAP32[2899] = $1; - return; - } - HEAP32[$5 + 4 >> 2] = $2 & -2; - HEAP32[$0 + 4 >> 2] = $1 | 1; - HEAP32[$0 + $1 >> 2] = $1; - } - if ($1 >>> 0 <= 255) { - $2 = $1 >>> 3 | 0; - $1 = ($2 << 3) + 11628 | 0; - $3 = HEAP32[2897]; - $2 = 1 << $2; - label$30 : { - if (!($3 & $2)) { - HEAP32[2897] = $2 | $3; - $2 = $1; - break label$30; - } - $2 = HEAP32[$1 + 8 >> 2]; - } - HEAP32[$1 + 8 >> 2] = $0; - HEAP32[$2 + 12 >> 2] = $0; - HEAP32[$0 + 12 >> 2] = $1; - HEAP32[$0 + 8 >> 2] = $2; - return; - } - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - $3 = $0; - $4 = $1 >>> 8 | 0; - $2 = 0; - label$32 : { - if (!$4) { - break label$32 - } - $2 = 31; - if ($1 >>> 0 > 16777215) { - break label$32 - } - $6 = $4 + 1048320 >>> 16 & 8; - $4 = $4 << $6; - $2 = $4 + 520192 >>> 16 & 4; - $5 = $4 << $2; - $4 = $5 + 245760 >>> 16 & 2; - $2 = ($5 << $4 >>> 15 | 0) - ($4 | ($2 | $6)) | 0; - $2 = ($2 << 1 | $1 >>> $2 + 21 & 1) + 28 | 0; - } - HEAP32[$3 + 28 >> 2] = $2; - $4 = ($2 << 2) + 11892 | 0; - label$33 : { - $3 = HEAP32[2898]; - $6 = 1 << $2; - label$34 : { - if (!($3 & $6)) { - HEAP32[2898] = $3 | $6; - HEAP32[$4 >> 2] = $0; - break label$34; - } - $3 = $1 << (($2 | 0) == 31 ? 0 : 25 - ($2 >>> 1 | 0) | 0); - $2 = HEAP32[$4 >> 2]; - while (1) { - $4 = $2; - if ((HEAP32[$2 + 4 >> 2] & -8) == ($1 | 0)) { - break label$33 - } - $2 = $3 >>> 29 | 0; - $3 = $3 << 1; - $6 = ($4 + ($2 & 4) | 0) + 16 | 0; - $2 = HEAP32[$6 >> 2]; - if ($2) { - continue - } - break; - }; - HEAP32[$6 >> 2] = $0; - } - HEAP32[$0 + 24 >> 2] = $4; - HEAP32[$0 + 12 >> 2] = $0; - HEAP32[$0 + 8 >> 2] = $0; - return; - } - $1 = HEAP32[$4 + 8 >> 2]; - HEAP32[$1 + 12 >> 2] = $0; - HEAP32[$4 + 8 >> 2] = $0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = $4; - HEAP32[$0 + 8 >> 2] = $1; - } - } - - function memchr($0, $1) { - var $2 = 0; - $2 = ($1 | 0) != 0; - label$1 : { - label$2 : { - label$3 : { - if (!$1 | !($0 & 3)) { - break label$3 - } - while (1) { - if (HEAPU8[$0 | 0] == 79) { - break label$2 - } - $0 = $0 + 1 | 0; - $1 = $1 + -1 | 0; - $2 = ($1 | 0) != 0; - if (!$1) { - break label$3 - } - if ($0 & 3) { - continue - } - break; - }; - } - if (!$2) { - break label$1 - } - } - label$5 : { - if (HEAPU8[$0 | 0] == 79 | $1 >>> 0 < 4) { - break label$5 - } - while (1) { - $2 = HEAP32[$0 >> 2] ^ 1330597711; - if (($2 ^ -1) & $2 + -16843009 & -2139062144) { - break label$5 - } - $0 = $0 + 4 | 0; - $1 = $1 + -4 | 0; - if ($1 >>> 0 > 3) { - continue - } - break; - }; - } - if (!$1) { - break label$1 - } - while (1) { - if (HEAPU8[$0 | 0] == 79) { - return $0 - } - $0 = $0 + 1 | 0; - $1 = $1 + -1 | 0; - if ($1) { - continue - } - break; - }; - } - return 0; - } - - function frexp($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - wasm2js_scratch_store_f64(+$0); - $2 = wasm2js_scratch_load_i32(1) | 0; - $3 = wasm2js_scratch_load_i32(0) | 0; - $4 = $2; - $2 = $2 >>> 20 & 2047; - if (($2 | 0) != 2047) { - if (!$2) { - $2 = $1; - if ($0 == 0.0) { - $1 = 0 - } else { - $0 = frexp($0 * 18446744073709551615.0, $1); - $1 = HEAP32[$1 >> 2] + -64 | 0; - } - HEAP32[$2 >> 2] = $1; - return $0; - } - HEAP32[$1 >> 2] = $2 + -1022; - wasm2js_scratch_store_i32(0, $3 | 0); - wasm2js_scratch_store_i32(1, $4 & -2146435073 | 1071644672); - $0 = +wasm2js_scratch_load_f64(); - } - return $0; - } - - function __ashlti3($0, $1, $2, $3, $4, $5) { - var $6 = 0, $7 = 0, $8 = 0, $9 = 0; - label$1 : { - if ($5 & 64) { - $3 = $1; - $4 = $5 + -64 | 0; - $1 = $4 & 31; - if (32 <= ($4 & 63) >>> 0) { - $4 = $3 << $1; - $3 = 0; - } else { - $4 = (1 << $1) - 1 & $3 >>> 32 - $1 | $2 << $1; - $3 = $3 << $1; - } - $1 = 0; - $2 = 0; - break label$1; - } - if (!$5) { - break label$1 - } - $6 = $3; - $8 = $5; - $3 = $5 & 31; - if (32 <= ($5 & 63) >>> 0) { - $7 = $6 << $3; - $9 = 0; - } else { - $7 = (1 << $3) - 1 & $6 >>> 32 - $3 | $4 << $3; - $9 = $6 << $3; - } - $3 = $2; - $6 = $1; - $5 = 64 - $5 | 0; - $4 = $5 & 31; - if (32 <= ($5 & 63) >>> 0) { - $5 = 0; - $3 = $3 >>> $4 | 0; - } else { - $5 = $3 >>> $4 | 0; - $3 = ((1 << $4) - 1 & $3) << 32 - $4 | $6 >>> $4; - } - $3 = $9 | $3; - $4 = $5 | $7; - $5 = $1; - $1 = $8 & 31; - if (32 <= ($8 & 63) >>> 0) { - $7 = $5 << $1; - $1 = 0; - } else { - $7 = (1 << $1) - 1 & $5 >>> 32 - $1 | $2 << $1; - $1 = $5 << $1; - } - $2 = $7; - } - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $4; - } - - function __lshrti3($0, $1, $2, $3, $4, $5) { - var $6 = 0, $7 = 0, $8 = 0, $9 = 0; - label$1 : { - if ($5 & 64) { - $2 = $5 + -64 | 0; - $1 = $2 & 31; - if (32 <= ($2 & 63) >>> 0) { - $2 = 0; - $1 = $4 >>> $1 | 0; - } else { - $2 = $4 >>> $1 | 0; - $1 = ((1 << $1) - 1 & $4) << 32 - $1 | $3 >>> $1; - } - $3 = 0; - $4 = 0; - break label$1; - } - if (!$5) { - break label$1 - } - $7 = $4; - $8 = $3; - $9 = 64 - $5 | 0; - $6 = $9 & 31; - if (32 <= ($9 & 63) >>> 0) { - $7 = $8 << $6; - $9 = 0; - } else { - $7 = (1 << $6) - 1 & $8 >>> 32 - $6 | $7 << $6; - $9 = $8 << $6; - } - $8 = $1; - $6 = $5; - $1 = $6 & 31; - if (32 <= ($6 & 63) >>> 0) { - $6 = 0; - $1 = $2 >>> $1 | 0; - } else { - $6 = $2 >>> $1 | 0; - $1 = ((1 << $1) - 1 & $2) << 32 - $1 | $8 >>> $1; - } - $1 = $9 | $1; - $2 = $6 | $7; - $6 = $3; - $3 = $5 & 31; - if (32 <= ($5 & 63) >>> 0) { - $7 = 0; - $3 = $4 >>> $3 | 0; - } else { - $7 = $4 >>> $3 | 0; - $3 = ((1 << $3) - 1 & $4) << 32 - $3 | $6 >>> $3; - } - $4 = $7; - } - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $4; - } - - function __trunctfdf2($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; - $6 = global$0 - 32 | 0; - global$0 = $6; - $4 = $3 & 2147483647; - $8 = $4; - $4 = $4 + -1006698496 | 0; - $7 = $2; - $5 = $2; - if ($2 >>> 0 < 0) { - $4 = $4 + 1 | 0 - } - $9 = $5; - $5 = $4; - $4 = $8 + -1140785152 | 0; - $10 = $7; - if ($7 >>> 0 < 0) { - $4 = $4 + 1 | 0 - } - label$1 : { - if (($4 | 0) == ($5 | 0) & $9 >>> 0 < $10 >>> 0 | $5 >>> 0 < $4 >>> 0) { - $4 = $3 << 4 | $2 >>> 28; - $2 = $2 << 4 | $1 >>> 28; - $1 = $1 & 268435455; - $7 = $1; - if (($1 | 0) == 134217728 & $0 >>> 0 >= 1 | $1 >>> 0 > 134217728) { - $4 = $4 + 1073741824 | 0; - $0 = $2 + 1 | 0; - if ($0 >>> 0 < 1) { - $4 = $4 + 1 | 0 - } - $5 = $0; - break label$1; - } - $5 = $2; - $4 = $4 - (($2 >>> 0 < 0) + -1073741824 | 0) | 0; - if ($0 | $7 ^ 134217728) { - break label$1 - } - $0 = $5 + ($5 & 1) | 0; - if ($0 >>> 0 < $5 >>> 0) { - $4 = $4 + 1 | 0 - } - $5 = $0; - break label$1; - } - if (!(!$7 & ($8 | 0) == 2147418112 ? !($0 | $1) : ($8 | 0) == 2147418112 & $7 >>> 0 < 0 | $8 >>> 0 < 2147418112)) { - $4 = $3 << 4 | $2 >>> 28; - $5 = $2 << 4 | $1 >>> 28; - $4 = $4 & 524287 | 2146959360; - break label$1; - } - $5 = 0; - $4 = 2146435072; - if ($8 >>> 0 > 1140785151) { - break label$1 - } - $4 = 0; - $7 = $8 >>> 16 | 0; - if ($7 >>> 0 < 15249) { - break label$1 - } - $4 = $3 & 65535 | 65536; - __ashlti3($6 + 16 | 0, $0, $1, $2, $4, $7 + -15233 | 0); - __lshrti3($6, $0, $1, $2, $4, 15361 - $7 | 0); - $2 = HEAP32[$6 + 4 >> 2]; - $0 = HEAP32[$6 + 8 >> 2]; - $4 = HEAP32[$6 + 12 >> 2] << 4 | $0 >>> 28; - $5 = $0 << 4 | $2 >>> 28; - $0 = $2 & 268435455; - $2 = $0; - $1 = HEAP32[$6 >> 2] | ((HEAP32[$6 + 16 >> 2] | HEAP32[$6 + 24 >> 2]) != 0 | (HEAP32[$6 + 20 >> 2] | HEAP32[$6 + 28 >> 2]) != 0); - if (($0 | 0) == 134217728 & $1 >>> 0 >= 1 | $0 >>> 0 > 134217728) { - $0 = $5 + 1 | 0; - if ($0 >>> 0 < 1) { - $4 = $4 + 1 | 0 - } - $5 = $0; - break label$1; - } - if ($1 | $2 ^ 134217728) { - break label$1 - } - $0 = $5 + ($5 & 1) | 0; - if ($0 >>> 0 < $5 >>> 0) { - $4 = $4 + 1 | 0 - } - $5 = $0; - } - global$0 = $6 + 32 | 0; - wasm2js_scratch_store_i32(0, $5 | 0); - wasm2js_scratch_store_i32(1, $3 & -2147483648 | $4); - return +wasm2js_scratch_load_f64(); - } - - function FLAC__crc8($0, $1) { - var $2 = 0; - if ($1) { - while (1) { - $2 = HEAPU8[(HEAPU8[$0 | 0] ^ $2) + 1024 | 0]; - $0 = $0 + 1 | 0; - $1 = $1 + -1 | 0; - if ($1) { - continue - } - break; - } - } - return $2; - } - - function FLAC__crc16($0, $1) { - var $2 = 0, $3 = 0; - if ($1 >>> 0 > 7) { - while (1) { - $3 = $2; - $2 = HEAPU8[$0 | 0] | HEAPU8[$0 + 1 | 0] << 8; - $2 = $3 ^ ($2 << 8 & 16711680 | $2 << 24) >>> 16; - $2 = HEAPU16[(HEAPU8[$0 + 7 | 0] << 1) + 1280 >> 1] ^ (HEAPU16[((HEAPU8[$0 + 6 | 0] << 1) + 1280 | 0) + 512 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 5 | 0] << 1) + 2304 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 4 | 0] << 1) + 2816 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 3 | 0] << 1) + 3328 >> 1] ^ (HEAPU16[(HEAPU8[$0 + 2 | 0] << 1) + 3840 >> 1] ^ (HEAPU16[(($2 & 255) << 1) + 4352 >> 1] ^ HEAPU16[($2 >>> 7 & 510) + 4864 >> 1])))))); - $0 = $0 + 8 | 0; - $1 = $1 + -8 | 0; - if ($1 >>> 0 > 7) { - continue - } - break; - } - } - if ($1) { - while (1) { - $2 = HEAPU16[((HEAPU8[$0 | 0] ^ ($2 & 65280) >>> 8) << 1) + 1280 >> 1] ^ $2 << 8; - $0 = $0 + 1 | 0; - $1 = $1 + -1 | 0; - if ($1) { - continue - } - break; - } - } - return $2 & 65535; - } - - function FLAC__crc16_update_words32($0, $1, $2) { - var $3 = 0; - if ($1 >>> 0 >= 2) { - while (1) { - $3 = $2; - $2 = HEAP32[$0 >> 2]; - $3 = $3 ^ $2 >>> 16; - $3 = HEAPU16[(($3 & 255) << 1) + 4352 >> 1] ^ HEAPU16[($3 >>> 7 & 510) + 4864 >> 1] ^ HEAPU16[($2 >>> 7 & 510) + 3840 >> 1] ^ HEAPU16[(($2 & 255) << 1) + 3328 >> 1]; - $2 = HEAP32[$0 + 4 >> 2]; - $2 = $3 ^ HEAPU16[($2 >>> 23 & 510) + 2816 >> 1] ^ HEAPU16[($2 >>> 15 & 510) + 2304 >> 1] ^ HEAPU16[(($2 >>> 7 & 510) + 1280 | 0) + 512 >> 1] ^ HEAPU16[(($2 & 255) << 1) + 1280 >> 1]; - $0 = $0 + 8 | 0; - $1 = $1 + -2 | 0; - if ($1 >>> 0 > 1) { - continue - } - break; - } - } - if ($1) { - $0 = HEAP32[$0 >> 2]; - $1 = $0 >>> 16 ^ $2; - $2 = HEAPU16[(($1 & 255) << 1) + 2304 >> 1] ^ HEAPU16[($1 >>> 7 & 510) + 2816 >> 1] ^ HEAPU16[(($0 >>> 7 & 510) + 1280 | 0) + 512 >> 1] ^ HEAPU16[(($0 & 255) << 1) + 1280 >> 1]; - } - return $2 & 65535; - } - - function memmove($0, $1, $2) { - var $3 = 0; - label$1 : { - if (($0 | 0) == ($1 | 0)) { - break label$1 - } - if (($1 - $0 | 0) - $2 >>> 0 <= 0 - ($2 << 1) >>> 0) { - memcpy($0, $1, $2); - return; - } - $3 = ($0 ^ $1) & 3; - label$3 : { - label$4 : { - if ($0 >>> 0 < $1 >>> 0) { - if ($3) { - break label$3 - } - if (!($0 & 3)) { - break label$4 - } - while (1) { - if (!$2) { - break label$1 - } - HEAP8[$0 | 0] = HEAPU8[$1 | 0]; - $1 = $1 + 1 | 0; - $2 = $2 + -1 | 0; - $0 = $0 + 1 | 0; - if ($0 & 3) { - continue - } - break; - }; - break label$4; - } - label$9 : { - if ($3) { - break label$9 - } - if ($0 + $2 & 3) { - while (1) { - if (!$2) { - break label$1 - } - $2 = $2 + -1 | 0; - $3 = $2 + $0 | 0; - HEAP8[$3 | 0] = HEAPU8[$1 + $2 | 0]; - if ($3 & 3) { - continue - } - break; - } - } - if ($2 >>> 0 <= 3) { - break label$9 - } - while (1) { - $2 = $2 + -4 | 0; - HEAP32[$2 + $0 >> 2] = HEAP32[$1 + $2 >> 2]; - if ($2 >>> 0 > 3) { - continue - } - break; - }; - } - if (!$2) { - break label$1 - } - while (1) { - $2 = $2 + -1 | 0; - HEAP8[$2 + $0 | 0] = HEAPU8[$1 + $2 | 0]; - if ($2) { - continue - } - break; - }; - break label$1; - } - if ($2 >>> 0 <= 3) { - break label$3 - } - while (1) { - HEAP32[$0 >> 2] = HEAP32[$1 >> 2]; - $1 = $1 + 4 | 0; - $0 = $0 + 4 | 0; - $2 = $2 + -4 | 0; - if ($2 >>> 0 > 3) { - continue - } - break; - }; - } - if (!$2) { - break label$1 - } - while (1) { - HEAP8[$0 | 0] = HEAPU8[$1 | 0]; - $0 = $0 + 1 | 0; - $1 = $1 + 1 | 0; - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break; - }; - } - } - - function FLAC__bitreader_delete($0) { - var $1 = 0; - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - dlfree($0); - } - - function FLAC__bitreader_free($0) { - var $1 = 0; - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 + 36 >> 2] = 0; - HEAP32[$0 + 40 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - } - - function FLAC__bitreader_init($0, $1) { - var $2 = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 2048; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - $2 = dlmalloc(8192); - HEAP32[$0 >> 2] = $2; - if (!$2) { - return 0 - } - HEAP32[$0 + 40 >> 2] = $1; - HEAP32[$0 + 36 >> 2] = 7; - return 1; - } - - function FLAC__bitreader_get_read_crc16($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $5 = HEAP32[$0 + 16 >> 2]; - $2 = HEAP32[$0 + 28 >> 2]; - label$1 : { - if ($5 >>> 0 <= $2 >>> 0) { - $4 = $2; - break label$1; - } - $1 = HEAP32[$0 + 32 >> 2]; - if (!$1) { - $4 = $2; - break label$1; - } - $4 = $2 + 1 | 0; - HEAP32[$0 + 28 >> 2] = $4; - $3 = HEAP32[$0 + 24 >> 2]; - if ($1 >>> 0 <= 31) { - $2 = HEAP32[HEAP32[$0 >> 2] + ($2 << 2) >> 2]; - while (1) { - $3 = HEAPU16[(($2 >>> 24 - $1 & 255 ^ $3 >>> 8) << 1) + 1280 >> 1] ^ $3 << 8 & 65280; - $7 = $1 >>> 0 < 24; - $6 = $1 + 8 | 0; - $1 = $6; - if ($7) { - continue - } - break; - }; - HEAP32[$0 + 32 >> 2] = $6; - } - HEAP32[$0 + 32 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = $3; - } - $1 = FLAC__crc16_update_words32(HEAP32[$0 >> 2] + ($4 << 2) | 0, $5 - $4 | 0, HEAPU16[$0 + 24 >> 1]); - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = $1; - $2 = HEAP32[$0 + 20 >> 2]; - label$6 : { - if (!$2) { - break label$6 - } - $3 = HEAP32[$0 + 32 >> 2]; - if ($3 >>> 0 >= $2 >>> 0) { - break label$6 - } - $4 = HEAP32[HEAP32[$0 >> 2] + (HEAP32[$0 + 16 >> 2] << 2) >> 2]; - while (1) { - $1 = HEAPU16[(($4 >>> 24 - $3 & 255 ^ $1 >>> 8) << 1) + 1280 >> 1] ^ $1 << 8 & 65280; - $3 = $3 + 8 | 0; - if ($3 >>> 0 < $2 >>> 0) { - continue - } - break; - }; - HEAP32[$0 + 32 >> 2] = $3; - HEAP32[$0 + 24 >> 2] = $1; - } - return $1; - } - - function FLAC__bitreader_is_consumed_byte_aligned($0) { - return !(HEAPU8[$0 + 20 | 0] & 7); - } - - function FLAC__bitreader_bits_left_for_byte_alignment($0) { - return 8 - (HEAP32[$0 + 20 >> 2] & 7) | 0; - } - - function FLAC__bitreader_read_raw_uint32($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0; - label$1 : { - if ($2) { - label$4 : { - while (1) { - $5 = HEAP32[$0 + 8 >> 2]; - $4 = HEAP32[$0 + 16 >> 2]; - $3 = HEAP32[$0 + 20 >> 2]; - if ((($5 - $4 << 5) + (HEAP32[$0 + 12 >> 2] << 3) | 0) - $3 >>> 0 >= $2 >>> 0) { - break label$4 - } - if (bitreader_read_from_client_($0)) { - continue - } - break; - }; - return 0; - } - if ($5 >>> 0 > $4 >>> 0) { - if ($3) { - $5 = HEAP32[$0 >> 2]; - $4 = HEAP32[$5 + ($4 << 2) >> 2] & -1 >>> $3; - $3 = 32 - $3 | 0; - if ($3 >>> 0 > $2 >>> 0) { - HEAP32[$1 >> 2] = $4 >>> $3 - $2; - HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; - break label$1; - } - HEAP32[$1 >> 2] = $4; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; - $2 = $2 - $3 | 0; - if (!$2) { - break label$1 - } - $3 = HEAP32[$1 >> 2] << $2; - HEAP32[$1 >> 2] = $3; - HEAP32[$1 >> 2] = $3 | HEAP32[(HEAP32[$0 + 16 >> 2] << 2) + $5 >> 2] >>> 32 - $2; - HEAP32[$0 + 20 >> 2] = $2; - return 1; - } - $3 = HEAP32[HEAP32[$0 >> 2] + ($4 << 2) >> 2]; - if ($2 >>> 0 <= 31) { - HEAP32[$1 >> 2] = $3 >>> 32 - $2; - HEAP32[$0 + 20 >> 2] = $2; - break label$1; - } - HEAP32[$1 >> 2] = $3; - HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; - return 1; - } - $4 = HEAP32[HEAP32[$0 >> 2] + ($4 << 2) >> 2]; - if ($3) { - HEAP32[$1 >> 2] = ($4 & -1 >>> $3) >>> 32 - ($2 + $3 | 0); - HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; - break label$1; - } - HEAP32[$1 >> 2] = $4 >>> 32 - $2; - HEAP32[$0 + 20 >> 2] = HEAP32[$0 + 20 >> 2] + $2; - break label$1; - } - HEAP32[$1 >> 2] = 0; - } - return 1; - } - - function bitreader_read_from_client_($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - $6 = global$0 - 16 | 0; - global$0 = $6; - $5 = HEAP32[$0 + 16 >> 2]; - label$1 : { - if (!$5) { - $2 = HEAP32[$0 + 8 >> 2]; - break label$1; - } - $1 = HEAP32[$0 + 28 >> 2]; - label$3 : { - if ($5 >>> 0 <= $1 >>> 0) { - $3 = $1; - break label$3; - } - $2 = HEAP32[$0 + 32 >> 2]; - if (!$2) { - $3 = $1; - break label$3; - } - $3 = $1 + 1 | 0; - HEAP32[$0 + 28 >> 2] = $3; - $4 = HEAP32[$0 + 24 >> 2]; - if ($2 >>> 0 <= 31) { - $1 = HEAP32[HEAP32[$0 >> 2] + ($1 << 2) >> 2]; - while (1) { - $4 = HEAPU16[(($1 >>> 24 - $2 & 255 ^ $4 >>> 8) << 1) + 1280 >> 1] ^ $4 << 8 & 65280; - $7 = $2 >>> 0 < 24; - $8 = $2 + 8 | 0; - $2 = $8; - if ($7) { - continue - } - break; - }; - HEAP32[$0 + 32 >> 2] = $8; - } - HEAP32[$0 + 32 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = $4; - } - $1 = FLAC__crc16_update_words32(HEAP32[$0 >> 2] + ($3 << 2) | 0, $5 - $3 | 0, HEAPU16[$0 + 24 >> 1]); - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = $1; - $3 = HEAP32[$0 >> 2]; - $1 = HEAP32[$0 + 16 >> 2]; - memmove($3, $3 + ($1 << 2) | 0, (HEAP32[$0 + 8 >> 2] - $1 | 0) + (HEAP32[$0 + 12 >> 2] != 0) << 2); - HEAP32[$0 + 16 >> 2] = 0; - $2 = HEAP32[$0 + 8 >> 2] - $1 | 0; - HEAP32[$0 + 8 >> 2] = $2; - } - $1 = HEAP32[$0 + 12 >> 2]; - $3 = (HEAP32[$0 + 4 >> 2] - $2 << 2) - $1 | 0; - HEAP32[$6 + 12 >> 2] = $3; - $4 = 0; - label$8 : { - if (!$3) { - break label$8 - } - $3 = HEAP32[$0 >> 2] + ($2 << 2) | 0; - $2 = $3 + $1 | 0; - if ($1) { - $1 = HEAP32[$3 >> 2]; - HEAP32[$3 >> 2] = $1 << 24 | $1 << 8 & 16711680 | ($1 >>> 8 & 65280 | $1 >>> 24); - } - if (!FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($2, $6 + 12 | 0, HEAP32[$0 + 40 >> 2])) { - break label$8 - } - $5 = HEAP32[$6 + 12 >> 2]; - $2 = HEAP32[$0 + 12 >> 2]; - $4 = HEAP32[$0 + 8 >> 2]; - $1 = $4 << 2; - $3 = ($5 + ($2 + $1 | 0) | 0) + 3 >>> 2 | 0; - $8 = $0; - if ($4 >>> 0 < $3 >>> 0) { - $2 = HEAP32[$0 >> 2]; - while (1) { - $7 = $2 + ($4 << 2) | 0; - $1 = HEAP32[$7 >> 2]; - HEAP32[$7 >> 2] = $1 << 8 & 16711680 | $1 << 24 | ($1 >>> 8 & 65280 | $1 >>> 24); - $4 = $4 + 1 | 0; - if (($3 | 0) != ($4 | 0)) { - continue - } - break; - }; - $2 = HEAP32[$0 + 12 >> 2]; - $1 = HEAP32[$0 + 8 >> 2] << 2; - } - $1 = $1 + ($2 + $5 | 0) | 0; - HEAP32[$8 + 12 >> 2] = $1 & 3; - HEAP32[$0 + 8 >> 2] = $1 >>> 2; - $4 = 1; - } - global$0 = $6 + 16 | 0; - return $4; - } - - function FLAC__bitreader_read_raw_int32($0, $1, $2) { - var $3 = 0, $4 = 0; - $3 = global$0 - 16 | 0; - global$0 = $3; - $4 = 0; - label$1 : { - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, $2)) { - break label$1 - } - $0 = 1 << $2 + -1; - HEAP32[$1 >> 2] = ($0 ^ HEAP32[$3 + 12 >> 2]) - $0; - $4 = 1; - } - $0 = $4; - global$0 = $3 + 16 | 0; - return $0; - } - - function FLAC__bitreader_read_raw_uint64($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0; - $3 = global$0 - 16 | 0; - global$0 = $3; - $4 = $1; - $5 = $1; - label$1 : { - label$2 : { - if ($2 >>> 0 >= 33) { - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, $2 + -32 | 0)) { - break label$1 - } - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, 32)) { - break label$1 - } - $0 = HEAP32[$3 + 12 >> 2]; - $2 = 0; - HEAP32[$1 >> 2] = $2; - HEAP32[$1 + 4 >> 2] = $0; - $1 = HEAP32[$3 + 8 >> 2] | $2; - break label$2; - } - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $2)) { - break label$1 - } - $0 = 0; - $1 = HEAP32[$3 + 8 >> 2]; - } - HEAP32[$5 >> 2] = $1; - HEAP32[$4 + 4 >> 2] = $0; - $6 = 1; - } - global$0 = $3 + 16 | 0; - return $6; - } - - function FLAC__bitreader_read_uint32_little_endian($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - HEAP32[$2 + 8 >> 2] = 0; - label$1 : { - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 8 | 0, 8)) { - break label$1 - } - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { - break label$1 - } - $3 = HEAP32[$2 + 8 >> 2] | HEAP32[$2 + 12 >> 2] << 8; - HEAP32[$2 + 8 >> 2] = $3; - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { - break label$1 - } - $3 = $3 | HEAP32[$2 + 12 >> 2] << 16; - HEAP32[$2 + 8 >> 2] = $3; - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { - break label$1 - } - $0 = $3 | HEAP32[$2 + 12 >> 2] << 24; - HEAP32[$2 + 8 >> 2] = $0; - HEAP32[$1 >> 2] = $0; - $4 = 1; - } - global$0 = $2 + 16 | 0; - return $4; - } - - function FLAC__bitreader_skip_bits_no_crc($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0; - $3 = global$0 - 16 | 0; - global$0 = $3; - $4 = 1; - label$1 : { - if (!$1) { - break label$1 - } - $2 = HEAP32[$0 + 20 >> 2] & 7; - label$2 : { - if ($2) { - $2 = 8 - $2 | 0; - $2 = $2 >>> 0 < $1 >>> 0 ? $2 : $1; - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $2)) { - break label$2 - } - $1 = $1 - $2 | 0; - } - $2 = $1 >>> 3 | 0; - if ($2) { - while (1) { - label$7 : { - if (!HEAP32[$0 + 20 >> 2]) { - if ($2 >>> 0 > 3) { - while (1) { - $5 = HEAP32[$0 + 16 >> 2]; - label$11 : { - if ($5 >>> 0 < HEAPU32[$0 + 8 >> 2]) { - HEAP32[$0 + 16 >> 2] = $5 + 1; - $2 = $2 + -4 | 0; - break label$11; - } - if (!bitreader_read_from_client_($0)) { - break label$2 - } - } - if ($2 >>> 0 > 3) { - continue - } - break; - }; - if (!$2) { - break label$7 - } - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, 8)) { - break label$2 - } - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break; - }; - break label$7; - } - if (!FLAC__bitreader_read_raw_uint32($0, $3 + 12 | 0, 8)) { - break label$2 - } - $2 = $2 + -1 | 0; - if ($2) { - continue - } - } - break; - }; - $1 = $1 & 7; - } - if (!$1) { - break label$1 - } - if (FLAC__bitreader_read_raw_uint32($0, $3 + 8 | 0, $1)) { - break label$1 - } - } - $4 = 0; - } - global$0 = $3 + 16 | 0; - return $4; - } - - function FLAC__bitreader_skip_byte_block_aligned_no_crc($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - $3 = 1; - label$1 : { - if (!$1) { - break label$1 - } - while (1) { - label$3 : { - if (!HEAP32[$0 + 20 >> 2]) { - label$5 : { - if ($1 >>> 0 < 4) { - break label$5 - } - while (1) { - $4 = HEAP32[$0 + 16 >> 2]; - label$7 : { - if ($4 >>> 0 < HEAPU32[$0 + 8 >> 2]) { - HEAP32[$0 + 16 >> 2] = $4 + 1; - $1 = $1 + -4 | 0; - break label$7; - } - if (!bitreader_read_from_client_($0)) { - break label$3 - } - } - if ($1 >>> 0 > 3) { - continue - } - break; - }; - if ($1) { - break label$5 - } - break label$1; - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { - break label$3 - } - $1 = $1 + -1 | 0; - if ($1) { - continue - } - break; - }; - break label$1; - } - if (!FLAC__bitreader_read_raw_uint32($0, $2 + 12 | 0, 8)) { - break label$3 - } - $1 = $1 + -1 | 0; - if ($1) { - continue - } - break label$1; - } - break; - }; - $3 = 0; - } - global$0 = $2 + 16 | 0; - return $3; - } - - function FLAC__bitreader_read_byte_block_aligned_no_crc($0, $1, $2) { - var $3 = 0, $4 = 0; - $4 = global$0 - 16 | 0; - global$0 = $4; - label$1 : { - if (!$2) { - $3 = 1; - break label$1; - } - while (1) { - if (!HEAP32[$0 + 20 >> 2]) { - label$5 : { - if ($2 >>> 0 < 4) { - break label$5 - } - while (1) { - label$7 : { - $3 = HEAP32[$0 + 16 >> 2]; - if ($3 >>> 0 < HEAPU32[$0 + 8 >> 2]) { - HEAP32[$0 + 16 >> 2] = $3 + 1; - $3 = HEAP32[HEAP32[$0 >> 2] + ($3 << 2) >> 2]; - $3 = $3 << 24 | $3 << 8 & 16711680 | ($3 >>> 8 & 65280 | $3 >>> 24); - HEAP8[$1 | 0] = $3; - HEAP8[$1 + 1 | 0] = $3 >>> 8; - HEAP8[$1 + 2 | 0] = $3 >>> 16; - HEAP8[$1 + 3 | 0] = $3 >>> 24; - $2 = $2 + -4 | 0; - $1 = $1 + 4 | 0; - break label$7; - } - if (bitreader_read_from_client_($0)) { - break label$7 - } - $3 = 0; - break label$1; - } - if ($2 >>> 0 > 3) { - continue - } - break; - }; - if ($2) { - break label$5 - } - $3 = 1; - break label$1; - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $4 + 12 | 0, 8)) { - $3 = 0; - break label$1; - } - HEAP8[$1 | 0] = HEAP32[$4 + 12 >> 2]; - $3 = 1; - $1 = $1 + 1 | 0; - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break; - }; - break label$1; - } - if (!FLAC__bitreader_read_raw_uint32($0, $4 + 12 | 0, 8)) { - $3 = 0; - break label$1; - } - HEAP8[$1 | 0] = HEAP32[$4 + 12 >> 2]; - $3 = 1; - $1 = $1 + 1 | 0; - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break; - }; - } - global$0 = $4 + 16 | 0; - return $3; - } - - function FLAC__bitreader_read_unary_unsigned($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - HEAP32[$1 >> 2] = 0; - label$1 : { - while (1) { - $3 = HEAP32[$0 + 16 >> 2]; - label$3 : { - if ($3 >>> 0 >= HEAPU32[$0 + 8 >> 2]) { - $2 = HEAP32[$0 + 20 >> 2]; - break label$3; - } - $2 = HEAP32[$0 + 20 >> 2]; - $4 = HEAP32[$0 >> 2]; - while (1) { - $3 = HEAP32[$4 + ($3 << 2) >> 2] << $2; - if ($3) { - $2 = $1; - $4 = HEAP32[$1 >> 2]; - $1 = Math_clz32($3); - HEAP32[$2 >> 2] = $4 + $1; - $2 = ($1 + HEAP32[$0 + 20 >> 2] | 0) + 1 | 0; - HEAP32[$0 + 20 >> 2] = $2; - $1 = 1; - if ($2 >>> 0 < 32) { - break label$1 - } - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = HEAP32[$0 + 16 >> 2] + 1; - return 1; - } - HEAP32[$1 >> 2] = (HEAP32[$1 >> 2] - $2 | 0) + 32; - $2 = 0; - HEAP32[$0 + 20 >> 2] = 0; - $3 = HEAP32[$0 + 16 >> 2] + 1 | 0; - HEAP32[$0 + 16 >> 2] = $3; - if ($3 >>> 0 < HEAPU32[$0 + 8 >> 2]) { - continue - } - break; - }; - } - $4 = HEAP32[$0 + 12 >> 2] << 3; - if ($4 >>> 0 > $2 >>> 0) { - $3 = (HEAP32[HEAP32[$0 >> 2] + ($3 << 2) >> 2] & -1 << 32 - $4) << $2; - if ($3) { - $2 = $1; - $4 = HEAP32[$1 >> 2]; - $1 = Math_clz32($3); - HEAP32[$2 >> 2] = $4 + $1; - HEAP32[$0 + 20 >> 2] = ($1 + HEAP32[$0 + 20 >> 2] | 0) + 1; - return 1; - } - HEAP32[$1 >> 2] = HEAP32[$1 >> 2] + ($4 - $2 | 0); - HEAP32[$0 + 20 >> 2] = $4; - } - if (bitreader_read_from_client_($0)) { - continue - } - break; - }; - $1 = 0; - } - return $1; - } - - function FLAC__bitreader_read_rice_signed_block($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; - $6 = global$0 - 16 | 0; - global$0 = $6; - $12 = ($2 << 2) + $1 | 0; - label$1 : { - if (!$3) { - $14 = 1; - if (($2 | 0) < 1) { - break label$1 - } - while (1) { - if (!FLAC__bitreader_read_unary_unsigned($0, $6 + 8 | 0)) { - $14 = 0; - break label$1; - } - $2 = HEAP32[$6 + 8 >> 2]; - HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); - $1 = $1 + 4 | 0; - if ($1 >>> 0 < $12 >>> 0) { - continue - } - break; - }; - break label$1; - } - label$5 : { - label$6 : { - $4 = HEAP32[$0 + 16 >> 2]; - $10 = HEAP32[$0 + 8 >> 2]; - if ($4 >>> 0 >= $10 >>> 0) { - break label$6 - } - $11 = HEAP32[$0 >> 2]; - $13 = HEAP32[$0 + 20 >> 2]; - $9 = HEAP32[$11 + ($4 << 2) >> 2] << $13; - $2 = 0; - break label$5; - } - $2 = 1; - } - while (1) { - label$9 : { - label$10 : { - label$11 : { - label$12 : { - if (!$2) { - $5 = 32 - $13 | 0; - label$14 : { - if ($1 >>> 0 < $12 >>> 0) { - $15 = 32 - $3 | 0; - while (1) { - $2 = $4; - $7 = $5; - label$17 : { - if ($9) { - $7 = Math_clz32($9); - $8 = $7; - break label$17; - } - while (1) { - $2 = $2 + 1 | 0; - if ($2 >>> 0 >= $10 >>> 0) { - break label$14 - } - $9 = HEAP32[($2 << 2) + $11 >> 2]; - $8 = Math_clz32($9); - $7 = $8 + $7 | 0; - if (!$9) { - continue - } - break; - }; - } - $4 = $9 << $8 << 1; - $8 = $4 >>> $15 | 0; - HEAP32[$6 + 8 >> 2] = $7; - $5 = ($7 ^ -1) + $5 & 31; - label$20 : { - if ($5 >>> 0 >= $3 >>> 0) { - $9 = $4 << $3; - $5 = $5 - $3 | 0; - $4 = $2; - break label$20; - } - $4 = $2 + 1 | 0; - if ($4 >>> 0 >= $10 >>> 0) { - break label$12 - } - $2 = HEAP32[($4 << 2) + $11 >> 2]; - $5 = $5 + $15 | 0; - $9 = $2 << 32 - $5; - $8 = $2 >>> $5 | $8; - } - HEAP32[$6 + 12 >> 2] = $8; - $2 = $7 << $3 | $8; - HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); - $1 = $1 + 4 | 0; - if ($1 >>> 0 < $12 >>> 0) { - continue - } - break; - }; - } - $1 = $4 >>> 0 < $10 >>> 0; - HEAP32[$0 + 16 >> 2] = ($1 & !$5) + $4; - HEAP32[$0 + 20 >> 2] = 32 - ($5 ? $5 : $1 << 5); - $14 = 1; - break label$1; - } - HEAP32[$0 + 20 >> 2] = 0; - $2 = $4 + 1 | 0; - HEAP32[$0 + 16 >> 2] = $10 >>> 0 > $2 >>> 0 ? $10 : $2; - break label$10; - } - if (!FLAC__bitreader_read_unary_unsigned($0, $6 + 8 | 0)) { - break label$1 - } - $7 = HEAP32[$6 + 8 >> 2] + $7 | 0; - HEAP32[$6 + 8 >> 2] = $7; - $8 = 0; - $5 = 0; - break label$11; - } - HEAP32[$0 + 16 >> 2] = $4; - HEAP32[$0 + 20 >> 2] = 0; - } - if (!FLAC__bitreader_read_raw_uint32($0, $6 + 12 | 0, $3 - $5 | 0)) { - break label$1 - } - $2 = $7 << $3; - $4 = HEAP32[$6 + 12 >> 2] | $8; - HEAP32[$6 + 12 >> 2] = $4; - $7 = 0; - $2 = $2 | $4; - HEAP32[$1 >> 2] = $2 >>> 1 ^ 0 - ($2 & 1); - $11 = HEAP32[$0 >> 2]; - $4 = HEAP32[$0 + 16 >> 2]; - $13 = HEAP32[$0 + 20 >> 2]; - $9 = HEAP32[$11 + ($4 << 2) >> 2] << $13; - $10 = HEAP32[$0 + 8 >> 2]; - $1 = $1 + 4 | 0; - if ($4 >>> 0 < $10 >>> 0 | $1 >>> 0 >= $12 >>> 0) { - break label$9 - } - } - $2 = 1; - continue; - } - $2 = 0; - continue; - }; - } - global$0 = $6 + 16 | 0; - return $14; - } - - function FLAC__bitreader_read_utf8_uint32($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $7 = global$0 - 16 | 0; - global$0 = $7; - label$1 : { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - break label$1 - } - $4 = HEAP32[$7 + 12 >> 2]; - if ($2) { - $5 = HEAP32[$3 >> 2]; - HEAP32[$3 >> 2] = $5 + 1; - HEAP8[$2 + $5 | 0] = $4; - } - label$3 : { - label$4 : { - label$5 : { - label$6 : { - if (!($4 & 128)) { - break label$6 - } - label$7 : { - if (!(!($4 & 192) | $4 & 32)) { - $6 = 31; - $5 = 1; - break label$7; - } - if (!(!($4 & 224) | $4 & 16)) { - $6 = 15; - $5 = 2; - break label$7; - } - if (!(!($4 & 240) | $4 & 8)) { - $6 = 7; - $5 = 3; - break label$7; - } - if ($4 & 248) { - $6 = 3; - $5 = 4; - if (!($4 & 4)) { - break label$7 - } - } - if (!($4 & 252) | $4 & 2) { - break label$5 - } - $6 = 1; - $5 = 5; - } - $4 = $4 & $6; - if (!$2) { - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - break label$1 - } - $2 = HEAP32[$7 + 12 >> 2]; - if (($2 & 192) != 128) { - break label$4 - } - $4 = $2 & 63 | $4 << 6; - $5 = $5 + -1 | 0; - if ($5) { - continue - } - break label$6; - } - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - break label$1 - } - $6 = HEAP32[$7 + 12 >> 2]; - $8 = HEAP32[$3 >> 2]; - HEAP32[$3 >> 2] = $8 + 1; - HEAP8[$2 + $8 | 0] = $6; - if (($6 & 192) != 128) { - break label$4 - } - $4 = $6 & 63 | $4 << 6; - $5 = $5 + -1 | 0; - if ($5) { - continue - } - break; - }; - } - HEAP32[$1 >> 2] = $4; - break label$3; - } - HEAP32[$1 >> 2] = -1; - break label$3; - } - HEAP32[$1 >> 2] = -1; - } - $9 = 1; - } - global$0 = $7 + 16 | 0; - return $9; - } - - function FLAC__bitreader_read_utf8_uint64($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $7 = global$0 - 16 | 0; - global$0 = $7; - label$1 : { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - break label$1 - } - $4 = HEAP32[$7 + 12 >> 2]; - if ($2) { - $6 = HEAP32[$3 >> 2]; - HEAP32[$3 >> 2] = $6 + 1; - HEAP8[$2 + $6 | 0] = $4; - } - label$4 : { - label$5 : { - label$6 : { - label$7 : { - if ($4 & 128) { - if (!(!($4 & 192) | $4 & 32)) { - $4 = $4 & 31; - $5 = 1; - break label$7; - } - if (!(!($4 & 224) | $4 & 16)) { - $4 = $4 & 15; - $5 = 2; - break label$7; - } - if (!(!($4 & 240) | $4 & 8)) { - $4 = $4 & 7; - $5 = 3; - break label$7; - } - if (!(!($4 & 248) | $4 & 4)) { - $4 = $4 & 3; - $5 = 4; - break label$7; - } - if (!(!($4 & 252) | $4 & 2)) { - $4 = $4 & 1; - $5 = 5; - break label$7; - } - $5 = 1; - if (!(!($4 & 254) | $4 & 1)) { - $5 = 6; - $4 = 0; - break label$7; - } - HEAP32[$1 >> 2] = -1; - HEAP32[$1 + 4 >> 2] = -1; - break label$1; - } - $6 = 0; - break label$6; - } - $6 = 0; - if (!$2) { - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - $5 = 0; - break label$1; - } - $2 = HEAP32[$7 + 12 >> 2]; - if (($2 & 192) != 128) { - break label$5 - } - $2 = $2 & 63; - $6 = $6 << 6 | $4 >>> 26; - $4 = $2 | $4 << 6; - $5 = $5 + -1 | 0; - if ($5) { - continue - } - break label$6; - } - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32($0, $7 + 12 | 0, 8)) { - $5 = 0; - break label$1; - } - $8 = HEAP32[$7 + 12 >> 2]; - $9 = HEAP32[$3 >> 2]; - HEAP32[$3 >> 2] = $9 + 1; - HEAP8[$2 + $9 | 0] = $8; - if (($8 & 192) != 128) { - break label$5 - } - $6 = $6 << 6 | $4 >>> 26; - $4 = $8 & 63 | $4 << 6; - $5 = $5 + -1 | 0; - if ($5) { - continue - } - break; - }; - } - HEAP32[$1 >> 2] = $4; - HEAP32[$1 + 4 >> 2] = $6; - break label$4; - } - HEAP32[$1 >> 2] = -1; - HEAP32[$1 + 4 >> 2] = -1; - } - $5 = 1; - } - global$0 = $7 + 16 | 0; - return $5; - } - - function qsort($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0; - $2 = global$0 - 208 | 0; - global$0 = $2; - HEAP32[$2 + 8 >> 2] = 1; - HEAP32[$2 + 12 >> 2] = 0; - label$1 : { - $5 = Math_imul($1, 24); - if (!$5) { - break label$1 - } - HEAP32[$2 + 16 >> 2] = 24; - HEAP32[$2 + 20 >> 2] = 24; - $1 = 24; - $4 = $1; - $3 = 2; - while (1) { - $6 = $4 + 24 | 0; - $4 = $1; - $1 = $1 + $6 | 0; - HEAP32[($2 + 16 | 0) + ($3 << 2) >> 2] = $1; - $3 = $3 + 1 | 0; - if ($1 >>> 0 < $5 >>> 0) { - continue - } - break; - }; - $4 = ($0 + $5 | 0) + -24 | 0; - label$3 : { - if ($4 >>> 0 <= $0 >>> 0) { - $3 = 1; - $1 = 1; - break label$3; - } - $3 = 1; - $1 = 1; - while (1) { - label$6 : { - if (($3 & 3) == 3) { - sift($0, $1, $2 + 16 | 0); - shr($2 + 8 | 0, 2); - $1 = $1 + 2 | 0; - break label$6; - } - $3 = $1 + -1 | 0; - label$8 : { - if (HEAPU32[($2 + 16 | 0) + ($3 << 2) >> 2] >= $4 - $0 >>> 0) { - trinkle($0, $2 + 8 | 0, $1, 0, $2 + 16 | 0); - break label$8; - } - sift($0, $1, $2 + 16 | 0); - } - if (($1 | 0) == 1) { - shl($2 + 8 | 0, 1); - $1 = 0; - break label$6; - } - shl($2 + 8 | 0, $3); - $1 = 1; - } - $3 = HEAP32[$2 + 8 >> 2] | 1; - HEAP32[$2 + 8 >> 2] = $3; - $0 = $0 + 24 | 0; - if ($0 >>> 0 < $4 >>> 0) { - continue - } - break; - }; - } - trinkle($0, $2 + 8 | 0, $1, 0, $2 + 16 | 0); - while (1) { - label$12 : { - label$13 : { - label$14 : { - if (!(($1 | 0) != 1 | ($3 | 0) != 1)) { - if (HEAP32[$2 + 12 >> 2]) { - break label$14 - } - break label$1; - } - if (($1 | 0) > 1) { - break label$13 - } - } - $4 = pntz($2 + 8 | 0); - shr($2 + 8 | 0, $4); - $3 = HEAP32[$2 + 8 >> 2]; - $1 = $1 + $4 | 0; - break label$12; - } - shl($2 + 8 | 0, 2); - HEAP32[$2 + 8 >> 2] = HEAP32[$2 + 8 >> 2] ^ 7; - shr($2 + 8 | 0, 1); - $5 = $0 + -24 | 0; - $4 = $1 + -2 | 0; - trinkle($5 - HEAP32[($2 + 16 | 0) + ($4 << 2) >> 2] | 0, $2 + 8 | 0, $1 + -1 | 0, 1, $2 + 16 | 0); - shl($2 + 8 | 0, 1); - $3 = HEAP32[$2 + 8 >> 2] | 1; - HEAP32[$2 + 8 >> 2] = $3; - trinkle($5, $2 + 8 | 0, $4, 1, $2 + 16 | 0); - $1 = $4; - } - $0 = $0 + -24 | 0; - continue; - }; - } - global$0 = $2 + 208 | 0; - } - - function sift($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $3 = global$0 - 240 | 0; - global$0 = $3; - HEAP32[$3 >> 2] = $0; - $6 = 1; - label$1 : { - if (($1 | 0) < 2) { - break label$1 - } - $4 = $0; - while (1) { - $5 = $4 + -24 | 0; - $7 = $1 + -2 | 0; - $4 = $5 - HEAP32[($7 << 2) + $2 >> 2] | 0; - if ((FUNCTION_TABLE[1]($0, $4) | 0) >= 0) { - if ((FUNCTION_TABLE[1]($0, $5) | 0) > -1) { - break label$1 - } - } - $0 = ($6 << 2) + $3 | 0; - label$4 : { - if ((FUNCTION_TABLE[1]($4, $5) | 0) >= 0) { - HEAP32[$0 >> 2] = $4; - $7 = $1 + -1 | 0; - break label$4; - } - HEAP32[$0 >> 2] = $5; - $4 = $5; - } - $6 = $6 + 1 | 0; - if (($7 | 0) < 2) { - break label$1 - } - $0 = HEAP32[$3 >> 2]; - $1 = $7; - continue; - }; - } - cycle($3, $6); - global$0 = $3 + 240 | 0; - } - - function shr($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $4 = $0; - label$1 : { - if ($1 >>> 0 <= 31) { - $2 = HEAP32[$0 >> 2]; - $3 = HEAP32[$0 + 4 >> 2]; - break label$1; - } - $2 = HEAP32[$0 + 4 >> 2]; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 >> 2] = $2; - $1 = $1 + -32 | 0; - $3 = 0; - } - HEAP32[$4 + 4 >> 2] = $3 >>> $1; - HEAP32[$0 >> 2] = $3 << 32 - $1 | $2 >>> $1; - } - - function trinkle($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0; - $5 = global$0 - 240 | 0; - global$0 = $5; - $6 = HEAP32[$1 >> 2]; - HEAP32[$5 + 232 >> 2] = $6; - $1 = HEAP32[$1 + 4 >> 2]; - HEAP32[$5 >> 2] = $0; - HEAP32[$5 + 236 >> 2] = $1; - $7 = 1; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - if ($1 ? 0 : ($6 | 0) == 1) { - break label$4 - } - $6 = $0 - HEAP32[($2 << 2) + $4 >> 2] | 0; - if ((FUNCTION_TABLE[1]($6, $0) | 0) < 1) { - break label$4 - } - $8 = !$3; - while (1) { - label$6 : { - $1 = $6; - if (!(!$8 | ($2 | 0) < 2)) { - $3 = HEAP32[(($2 << 2) + $4 | 0) + -8 >> 2]; - $6 = $0 + -24 | 0; - if ((FUNCTION_TABLE[1]($6, $1) | 0) > -1) { - break label$6 - } - if ((FUNCTION_TABLE[1]($6 - $3 | 0, $1) | 0) > -1) { - break label$6 - } - } - HEAP32[($7 << 2) + $5 >> 2] = $1; - $0 = pntz($5 + 232 | 0); - shr($5 + 232 | 0, $0); - $7 = $7 + 1 | 0; - $2 = $0 + $2 | 0; - if (HEAP32[$5 + 236 >> 2] ? 0 : HEAP32[$5 + 232 >> 2] == 1) { - break label$2 - } - $3 = 0; - $8 = 1; - $0 = $1; - $6 = $1 - HEAP32[($2 << 2) + $4 >> 2] | 0; - if ((FUNCTION_TABLE[1]($6, HEAP32[$5 >> 2]) | 0) > 0) { - continue - } - break label$3; - } - break; - }; - $1 = $0; - break label$2; - } - $1 = $0; - } - if ($3) { - break label$1 - } - } - cycle($5, $7); - sift($1, $2, $4); - } - global$0 = $5 + 240 | 0; - } - - function shl($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $4 = $0; - label$1 : { - if ($1 >>> 0 <= 31) { - $2 = HEAP32[$0 + 4 >> 2]; - $3 = HEAP32[$0 >> 2]; - break label$1; - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 >> 2] = 0; - $1 = $1 + -32 | 0; - $3 = 0; - } - HEAP32[$4 >> 2] = $3 << $1; - HEAP32[$0 + 4 >> 2] = $2 << $1 | $3 >>> 32 - $1; - } - - function pntz($0) { - var $1 = 0; - $1 = __wasm_ctz_i32(HEAP32[$0 >> 2] + -1 | 0); - if (!$1) { - $0 = __wasm_ctz_i32(HEAP32[$0 + 4 >> 2]); - return $0 ? $0 + 32 | 0 : 0; - } - return $1; - } - - function cycle($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $3 = 24; - $4 = global$0 - 256 | 0; - global$0 = $4; - label$1 : { - if (($1 | 0) < 2) { - break label$1 - } - $7 = ($1 << 2) + $0 | 0; - HEAP32[$7 >> 2] = $4; - $2 = $4; - while (1) { - $5 = $3 >>> 0 < 256 ? $3 : 256; - memcpy($2, HEAP32[$0 >> 2], $5); - $2 = 0; - while (1) { - $6 = ($2 << 2) + $0 | 0; - $2 = $2 + 1 | 0; - memcpy(HEAP32[$6 >> 2], HEAP32[($2 << 2) + $0 >> 2], $5); - HEAP32[$6 >> 2] = HEAP32[$6 >> 2] + $5; - if (($1 | 0) != ($2 | 0)) { - continue - } - break; - }; - $3 = $3 - $5 | 0; - if (!$3) { - break label$1 - } - $2 = HEAP32[$7 >> 2]; - continue; - }; - } - global$0 = $4 + 256 | 0; - } - - function FLAC__format_sample_rate_is_subset($0) { - if ($0 + -1 >>> 0 <= 655349) { - return !(($0 >>> 0) % 10) | (!(($0 >>> 0) % 1e3) | $0 >>> 0 < 65536) - } - return 0; - } - - function FLAC__format_seektable_is_legal($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $3 = HEAP32[$0 >> 2]; - if (!$3) { - return 1 - } - $6 = HEAP32[$0 + 4 >> 2]; - $0 = 0; - $4 = 1; - while (1) { - $7 = $2; - $5 = $1; - $1 = Math_imul($0, 24) + $6 | 0; - $2 = HEAP32[$1 >> 2]; - $1 = HEAP32[$1 + 4 >> 2]; - if (!(($2 | 0) == -1 & ($1 | 0) == -1 | $4 | (($1 | 0) == ($5 | 0) & $2 >>> 0 > $7 >>> 0 | $1 >>> 0 > $5 >>> 0))) { - return 0 - } - $4 = 0; - $0 = $0 + 1 | 0; - if ($0 >>> 0 < $3 >>> 0) { - continue - } - break; - }; - return 1; - } - - function FLAC__format_seektable_sort($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - label$1 : { - $2 = HEAP32[$0 >> 2]; - if (!$2) { - break label$1 - } - qsort(HEAP32[$0 + 4 >> 2], $2); - if (!HEAP32[$0 >> 2]) { - break label$1 - } - $2 = 1; - $1 = HEAP32[$0 >> 2]; - if ($1 >>> 0 > 1) { - $6 = 1; - while (1) { - $4 = HEAP32[$0 + 4 >> 2]; - $3 = $4 + Math_imul($6, 24) | 0; - $5 = HEAP32[$3 >> 2]; - $7 = HEAP32[$3 + 4 >> 2]; - $8 = $7; - label$4 : { - if (($5 | 0) != -1 | ($7 | 0) != -1) { - $7 = $5; - $5 = ($4 + Math_imul($2, 24) | 0) + -24 | 0; - if (($7 | 0) == HEAP32[$5 >> 2] & HEAP32[$5 + 4 >> 2] == ($8 | 0)) { - break label$4 - } - } - $5 = HEAP32[$3 + 4 >> 2]; - $1 = $4 + Math_imul($2, 24) | 0; - HEAP32[$1 >> 2] = HEAP32[$3 >> 2]; - HEAP32[$1 + 4 >> 2] = $5; - $4 = HEAP32[$3 + 20 >> 2]; - HEAP32[$1 + 16 >> 2] = HEAP32[$3 + 16 >> 2]; - HEAP32[$1 + 20 >> 2] = $4; - $4 = HEAP32[$3 + 12 >> 2]; - HEAP32[$1 + 8 >> 2] = HEAP32[$3 + 8 >> 2]; - HEAP32[$1 + 12 >> 2] = $4; - $2 = $2 + 1 | 0; - $1 = HEAP32[$0 >> 2]; - } - $6 = $6 + 1 | 0; - if ($6 >>> 0 < $1 >>> 0) { - continue - } - break; - }; - } - if ($2 >>> 0 >= $1 >>> 0) { - break label$1 - } - $3 = HEAP32[$0 + 4 >> 2]; - while (1) { - $0 = $3 + Math_imul($2, 24) | 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 >> 2] = -1; - HEAP32[$0 + 4 >> 2] = -1; - $2 = $2 + 1 | 0; - if (($1 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - } - - function seekpoint_compare_($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0, $3 = 0; - $2 = HEAP32[$0 + 4 >> 2]; - $3 = HEAP32[$1 + 4 >> 2]; - $0 = HEAP32[$0 >> 2]; - $1 = HEAP32[$1 >> 2]; - return (($0 | 0) == ($1 | 0) & ($2 | 0) == ($3 | 0) ? 0 : ($2 | 0) == ($3 | 0) & $0 >>> 0 < $1 >>> 0 | $2 >>> 0 < $3 >>> 0 ? -1 : 1) | 0; - } - - function utf8len_($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0; - $2 = 1; - label$1 : { - $1 = HEAPU8[$0 | 0]; - label$2 : { - if (!($1 & 128)) { - break label$2 - } - if (!(($1 & 224) != 192 | (HEAPU8[$0 + 1 | 0] & 192) != 128)) { - return (($1 & 254) != 192) << 1 - } - label$4 : { - if (($1 & 240) != 224) { - break label$4 - } - $3 = HEAPU8[$0 + 1 | 0]; - if (($3 & 192) != 128) { - break label$4 - } - $4 = HEAPU8[$0 + 2 | 0]; - if (($4 & 192) != 128) { - break label$4 - } - $2 = 0; - if (($3 & 224) == 128 ? ($1 | 0) == 224 : 0) { - break label$2 - } - label$5 : { - label$6 : { - switch ($1 + -237 | 0) { - case 0: - if (($3 & 224) != 160) { - break label$5 - } - break label$2; - case 2: - break label$6; - default: - break label$5; - }; - } - if (($3 | 0) != 191) { - break label$5 - } - if (($4 & 254) == 190) { - break label$2 - } - } - return 3; - } - label$8 : { - if (($1 & 248) != 240) { - break label$8 - } - $2 = HEAPU8[$0 + 1 | 0]; - if (($2 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128) { - break label$8 - } - if ((HEAPU8[$0 + 3 | 0] & 192) == 128) { - break label$1 - } - } - label$9 : { - if (($1 & 252) != 248) { - break label$9 - } - $2 = HEAPU8[$0 + 1 | 0]; - if (($2 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128 | ((HEAPU8[$0 + 3 | 0] & 192) != 128 | (HEAPU8[$0 + 4 | 0] & 192) != 128)) { - break label$9 - } - return ($1 | 0) == 248 ? (($2 & 248) == 128 ? 0 : 5) : 5; - } - $2 = 0; - if (($1 & 254) != 252) { - break label$2 - } - $3 = HEAPU8[$0 + 1 | 0]; - if (($3 & 192) != 128 | (HEAPU8[$0 + 2 | 0] & 192) != 128 | ((HEAPU8[$0 + 3 | 0] & 192) != 128 | (HEAPU8[$0 + 4 | 0] & 192) != 128)) { - break label$2 - } - if ((HEAPU8[$0 + 5 | 0] & 192) != 128) { - break label$2 - } - $2 = ($1 | 0) == 252 ? (($3 & 252) == 128 ? 0 : 6) : 6; - } - return $2; - } - return ($1 | 0) == 240 ? (($2 & 240) != 128) << 2 : 4; - } - - function FLAC__format_cuesheet_is_legal($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - if ($1) { - $1 = HEAP32[$0 + 140 >> 2]; - $3 = $1; - $2 = HEAP32[$0 + 136 >> 2]; - if (!$1 & $2 >>> 0 <= 88199 | $1 >>> 0 < 0) { - $0 = 0; - break label$1; - } - if (__wasm_i64_urem($2, $3) | i64toi32_i32$HIGH_BITS) { - $0 = 0; - break label$1; - } - $3 = HEAP32[$0 + 148 >> 2]; - if (!$3) { - break label$2 - } - if (HEAPU8[(HEAP32[$0 + 152 >> 2] + ($3 << 5) | 0) + -24 | 0] == 170) { - break label$7 - } - $0 = 0; - break label$1; - } - $2 = HEAP32[$0 + 148 >> 2]; - if (!$2) { - break label$2 - } - $4 = $2 + -1 | 0; - $6 = HEAP32[$0 + 152 >> 2]; - $1 = 0; - while (1) { - $0 = $6 + ($1 << 5) | 0; - if (!HEAPU8[$0 + 8 | 0]) { - break label$3 - } - $3 = HEAPU8[$0 + 23 | 0]; - label$12 : { - label$13 : { - if ($1 >>> 0 < $4 >>> 0) { - if (!$3) { - break label$4 - } - if (HEAPU8[HEAP32[$0 + 24 >> 2] + 8 | 0] > 1) { - break label$5 - } - break label$13; - } - if (!$3) { - break label$12 - } - } - $7 = $0 + 24 | 0; - $0 = 0; - while (1) { - if ($0) { - $5 = HEAP32[$7 >> 2] + ($0 << 4) | 0; - if ((HEAPU8[$5 + -8 | 0] + 1 | 0) != HEAPU8[$5 + 8 | 0]) { - break label$6 - } - } - $0 = $0 + 1 | 0; - if ($0 >>> 0 < $3 >>> 0) { - continue - } - break; - }; - } - $0 = 1; - $1 = $1 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - $6 = $3 + -1 | 0; - $7 = HEAP32[$0 + 152 >> 2]; - $1 = 0; - while (1) { - $0 = $7 + ($1 << 5) | 0; - $2 = HEAPU8[$0 + 8 | 0]; - if (!$2) { - break label$3 - } - if (!(($2 | 0) == 170 | $2 >>> 0 < 100)) { - $0 = 0; - break label$1; - } - if (__wasm_i64_urem(HEAP32[$0 >> 2], HEAP32[$0 + 4 >> 2]) | i64toi32_i32$HIGH_BITS) { - $0 = 0; - break label$1; - } - $2 = HEAPU8[$0 + 23 | 0]; - label$21 : { - label$22 : { - if ($1 >>> 0 < $6 >>> 0) { - if (!$2) { - break label$4 - } - if (HEAPU8[HEAP32[$0 + 24 >> 2] + 8 | 0] < 2) { - break label$22 - } - break label$5; - } - if (!$2) { - break label$21 - } - } - $5 = HEAP32[$0 + 24 >> 2]; - $0 = 0; - while (1) { - $4 = $5 + ($0 << 4) | 0; - if (__wasm_i64_urem(HEAP32[$4 >> 2], HEAP32[$4 + 4 >> 2]) | i64toi32_i32$HIGH_BITS) { - $0 = 0; - break label$1; - } - if (HEAPU8[$4 + 8 | 0] != (HEAPU8[$4 + -8 | 0] + 1 | 0) ? $0 : 0) { - break label$6 - } - $0 = $0 + 1 | 0; - if ($0 >>> 0 < $2 >>> 0) { - continue - } - break; - }; - } - $0 = 1; - $1 = $1 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - $0 = 0; - break label$1; - } - $0 = 0; - break label$1; - } - $0 = 0; - break label$1; - } - $0 = 0; - break label$1; - } - $0 = 0; - } - return $0; - } - - function FLAC__format_picture_is_legal($0) { - var $1 = 0, $2 = 0; - label$1 : { - label$2 : { - $2 = HEAP32[$0 + 4 >> 2]; - $1 = HEAPU8[$2 | 0]; - if (!$1) { - break label$2 - } - while (1) { - if (($1 + -32 & 255) >>> 0 < 95) { - $2 = $2 + 1 | 0; - $1 = HEAPU8[$2 | 0]; - if ($1) { - continue - } - break label$2; - } - break; - }; - $2 = 0; - break label$1; - } - $2 = 1; - $1 = HEAP32[$0 + 8 >> 2]; - if (!HEAPU8[$1 | 0]) { - break label$1 - } - while (1) { - $0 = utf8len_($1); - if (!$0) { - $2 = 0; - break label$1; - } - $1 = $0 + $1 | 0; - if (HEAPU8[$1 | 0]) { - continue - } - break; - }; - } - return $2; - } - - function FLAC__format_get_max_rice_partition_order_from_blocksize_limited_max_and_predictor_order($0, $1, $2) { - var $3 = 0; - while (1) { - $3 = $0; - if ($3) { - $0 = $3 + -1 | 0; - if ($1 >>> $3 >>> 0 <= $2 >>> 0) { - continue - } - } - break; - }; - return $3; - } - - function FLAC__format_get_max_rice_partition_order_from_blocksize($0) { - var $1 = 0, $2 = 0; - label$1 : { - if (!($0 & 1)) { - while (1) { - $1 = $1 + 1 | 0; - $2 = $0 & 2; - $0 = $0 >>> 1 | 0; - if (!$2) { - continue - } - break; - }; - $0 = 15; - if ($1 >>> 0 > 14) { - break label$1 - } - } - $0 = $1; - } - return $0; - } - - function FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0) { - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - } - - function FLAC__format_entropy_coding_method_partitioned_rice_contents_clear($0) { - var $1 = 0; - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 4 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - } - - function FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0; - $3 = 1; - label$1 : { - if (HEAPU32[$0 + 8 >> 2] >= $1 >>> 0) { - break label$1 - } - $3 = HEAP32[$0 >> 2]; - $4 = 4 << $1; - $2 = dlrealloc($3, $4); - if (!($2 | $1 >>> 0 > 29)) { - dlfree($3) - } - HEAP32[$0 >> 2] = $2; - $3 = 0; - if (!$2) { - break label$1 - } - $5 = HEAP32[$0 + 4 >> 2]; - $2 = dlrealloc($5, $4); - if (!($2 | $1 >>> 0 > 29)) { - dlfree($5) - } - HEAP32[$0 + 4 >> 2] = $2; - if (!$2) { - break label$1 - } - memset($2, $4); - HEAP32[$0 + 8 >> 2] = $1; - $3 = 1; - } - return $3; - } - - function ogg_page_serialno($0) { - $0 = HEAP32[$0 >> 2]; - return HEAPU8[$0 + 14 | 0] | HEAPU8[$0 + 15 | 0] << 8 | (HEAPU8[$0 + 16 | 0] << 16 | HEAPU8[$0 + 17 | 0] << 24); - } - - function ogg_stream_init($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - if ($0) { - memset($0 + 8 | 0, 352); - HEAP32[$0 + 24 >> 2] = 1024; - HEAP32[$0 + 4 >> 2] = 16384; - $3 = dlmalloc(16384); - HEAP32[$0 >> 2] = $3; - $2 = dlmalloc(4096); - HEAP32[$0 + 16 >> 2] = $2; - $4 = dlmalloc(8192); - HEAP32[$0 + 20 >> 2] = $4; - label$2 : { - if ($3) { - if ($2 ? $4 : 0) { - break label$2 - } - dlfree($3); - $2 = HEAP32[$0 + 16 >> 2]; - } - if ($2) { - dlfree($2) - } - $1 = HEAP32[$0 + 20 >> 2]; - if ($1) { - dlfree($1) - } - memset($0, 360); - return -1; - } - HEAP32[$0 + 336 >> 2] = $1; - $0 = 0; - } else { - $0 = -1 - } - return $0; - } - - function ogg_stream_clear($0) { - var $1 = 0; - if ($0) { - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 16 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 20 >> 2]; - if ($1) { - dlfree($1) - } - memset($0, 360); - } - } - - function ogg_page_checksum_set($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0; - if ($0) { - HEAP8[HEAP32[$0 >> 2] + 22 | 0] = 0; - HEAP8[HEAP32[$0 >> 2] + 23 | 0] = 0; - HEAP8[HEAP32[$0 >> 2] + 24 | 0] = 0; - HEAP8[HEAP32[$0 >> 2] + 25 | 0] = 0; - $3 = HEAP32[$0 + 4 >> 2]; - if (($3 | 0) >= 1) { - $4 = HEAP32[$0 >> 2]; - while (1) { - $1 = HEAP32[((HEAPU8[$2 + $4 | 0] ^ $1 >>> 24) << 2) + 6512 >> 2] ^ $1 << 8; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - $3 = HEAP32[$0 + 12 >> 2]; - if (($3 | 0) >= 1) { - $4 = HEAP32[$0 + 8 >> 2]; - $2 = 0; - while (1) { - $1 = HEAP32[((HEAPU8[$2 + $4 | 0] ^ $1 >>> 24) << 2) + 6512 >> 2] ^ $1 << 8; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - HEAP8[HEAP32[$0 >> 2] + 22 | 0] = $1; - HEAP8[HEAP32[$0 >> 2] + 23 | 0] = $1 >>> 8; - HEAP8[HEAP32[$0 >> 2] + 24 | 0] = $1 >>> 16; - HEAP8[HEAP32[$0 >> 2] + 25 | 0] = $1 >>> 24; - } - } - - function ogg_stream_iovecin($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; - $6 = -1; - folding_inner0 : { - label$1 : { - if (!$0) { - break label$1 - } - $8 = HEAP32[$0 >> 2]; - if (!$8) { - break label$1 - } - if (!$1) { - return 0 - } - while (1) { - $7 = HEAP32[(($5 << 3) + $1 | 0) + 4 >> 2]; - if (($7 | 0) < 0 | ($9 | 0) > (2147483647 - $7 | 0)) { - break label$1 - } - $9 = $7 + $9 | 0; - $5 = $5 + 1 | 0; - if (($5 | 0) != 1) { - continue - } - break; - }; - $5 = HEAP32[$0 + 12 >> 2]; - if ($5) { - $7 = HEAP32[$0 + 8 >> 2] - $5 | 0; - HEAP32[$0 + 8 >> 2] = $7; - if ($7) { - memmove($8, $5 + $8 | 0, $7) - } - HEAP32[$0 + 12 >> 2] = 0; - } - $5 = HEAP32[$0 + 4 >> 2]; - if (($5 - $9 | 0) <= HEAP32[$0 + 8 >> 2]) { - if (($5 | 0) > (2147483647 - $9 | 0)) { - break folding_inner0 - } - $5 = $5 + $9 | 0; - $5 = ($5 | 0) < 2147482623 ? $5 + 1024 | 0 : $5; - $8 = dlrealloc(HEAP32[$0 >> 2], $5); - if (!$8) { - break folding_inner0 - } - HEAP32[$0 >> 2] = $8; - HEAP32[$0 + 4 >> 2] = $5; - } - $8 = ($9 | 0) / 255 | 0; - $11 = $8 + 1 | 0; - if (_os_lacing_expand($0, $11)) { - break label$1 - } - $6 = HEAP32[$0 + 8 >> 2]; - $5 = 0; - while (1) { - $7 = HEAP32[$0 >> 2] + $6 | 0; - $6 = ($5 << 3) + $1 | 0; - memcpy($7, HEAP32[$6 >> 2], HEAP32[$6 + 4 >> 2]); - $6 = HEAP32[$0 + 8 >> 2] + HEAP32[$6 + 4 >> 2] | 0; - HEAP32[$0 + 8 >> 2] = $6; - $5 = $5 + 1 | 0; - if (($5 | 0) != 1) { - continue - } - break; - }; - $7 = HEAP32[$0 + 16 >> 2]; - $12 = $7; - $1 = HEAP32[$0 + 28 >> 2]; - $13 = $1; - label$19 : { - if (($9 | 0) <= 254) { - $6 = HEAP32[$0 + 20 >> 2]; - $5 = 0; - break label$19; - } - $6 = HEAP32[$0 + 20 >> 2]; - $5 = 0; - while (1) { - $10 = $1 + $5 | 0; - HEAP32[$7 + ($10 << 2) >> 2] = 255; - $14 = HEAP32[$0 + 356 >> 2]; - $10 = ($10 << 3) + $6 | 0; - HEAP32[$10 >> 2] = HEAP32[$0 + 352 >> 2]; - HEAP32[$10 + 4 >> 2] = $14; - $5 = $5 + 1 | 0; - if (($8 | 0) != ($5 | 0)) { - continue - } - break; - }; - $5 = $8; - } - $5 = $13 + $5 | 0; - HEAP32[$12 + ($5 << 2) >> 2] = $9 - Math_imul($8, 255); - $5 = ($5 << 3) + $6 | 0; - HEAP32[$5 >> 2] = $3; - HEAP32[$5 + 4 >> 2] = $4; - HEAP32[$0 + 352 >> 2] = $3; - HEAP32[$0 + 356 >> 2] = $4; - $3 = $7 + ($1 << 2) | 0; - HEAP32[$3 >> 2] = HEAP32[$3 >> 2] | 256; - HEAP32[$0 + 28 >> 2] = $1 + $11; - $1 = HEAP32[$0 + 348 >> 2]; - $3 = HEAP32[$0 + 344 >> 2] + 1 | 0; - if ($3 >>> 0 < 1) { - $1 = $1 + 1 | 0 - } - HEAP32[$0 + 344 >> 2] = $3; - HEAP32[$0 + 348 >> 2] = $1; - $6 = 0; - if (!$2) { - break label$1 - } - HEAP32[$0 + 328 >> 2] = 1; - } - return $6; - } - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 16 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 20 >> 2]; - if ($1) { - dlfree($1) - } - memset($0, 360); - return -1; - } - - function _os_lacing_expand($0, $1) { - var $2 = 0; - folding_inner0 : { - $2 = HEAP32[$0 + 24 >> 2]; - if (($2 - $1 | 0) <= HEAP32[$0 + 28 >> 2]) { - if (($2 | 0) > (2147483647 - $1 | 0)) { - break folding_inner0 - } - $1 = $1 + $2 | 0; - $1 = ($1 | 0) < 2147483615 ? $1 + 32 | 0 : $1; - $2 = dlrealloc(HEAP32[$0 + 16 >> 2], $1 << 2); - if (!$2) { - break folding_inner0 - } - HEAP32[$0 + 16 >> 2] = $2; - $2 = dlrealloc(HEAP32[$0 + 20 >> 2], $1 << 3); - if (!$2) { - break folding_inner0 - } - HEAP32[$0 + 24 >> 2] = $1; - HEAP32[$0 + 20 >> 2] = $2; - } - return 0; - } - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 16 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 20 >> 2]; - if ($1) { - dlfree($1) - } - memset($0, 360); - return -1; - } - - function ogg_stream_packetin($0, $1) { - var $2 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - HEAP32[$2 + 8 >> 2] = HEAP32[$1 >> 2]; - HEAP32[$2 + 12 >> 2] = HEAP32[$1 + 4 >> 2]; - $0 = ogg_stream_iovecin($0, $2 + 8 | 0, HEAP32[$1 + 12 >> 2], HEAP32[$1 + 16 >> 2], HEAP32[$1 + 20 >> 2]); - global$0 = $2 + 16 | 0; - return $0; - } - - function ogg_stream_flush_i($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; - label$1 : { - if (!$0) { - break label$1 - } - $8 = HEAP32[$0 + 28 >> 2]; - $4 = ($8 | 0) < 255 ? $8 : 255; - if (!$4) { - break label$1 - } - $10 = HEAP32[$0 >> 2]; - if (!$10) { - break label$1 - } - label$2 : { - label$3 : { - label$4 : { - $11 = HEAP32[$0 + 332 >> 2]; - if ($11) { - if (($8 | 0) >= 1) { - break label$4 - } - $7 = -1; - $5 = -1; - break label$3; - } - $3 = ($4 | 0) > 0 ? $4 : 0; - while (1) { - if (($3 | 0) == ($6 | 0)) { - break label$3 - } - $9 = $6 << 2; - $4 = $6 + 1 | 0; - $6 = $4; - if (HEAPU8[$9 + HEAP32[$0 + 16 >> 2] | 0] == 255) { - continue - } - break; - }; - $3 = $4; - break label$3; - } - $4 = ($4 | 0) > 1 ? $4 : 1; - $7 = -1; - $5 = -1; - label$7 : { - while (1) { - if (!(($6 | 0) <= 4096 | ($9 | 0) <= 3)) { - $2 = 1; - break label$7; - } - $9 = 0; - $12 = HEAPU8[HEAP32[$0 + 16 >> 2] + ($3 << 2) | 0]; - if (($12 | 0) != 255) { - $13 = $13 + 1 | 0; - $9 = $13; - $5 = HEAP32[$0 + 20 >> 2] + ($3 << 3) | 0; - $7 = HEAP32[$5 >> 2]; - $5 = HEAP32[$5 + 4 >> 2]; - } - $6 = $6 + $12 | 0; - $3 = $3 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - $3 = $4; - } - $4 = 255; - if (($3 | 0) == 255) { - break label$2 - } - } - $4 = $3; - if (!$2) { - break label$1 - } - } - HEAP32[$0 + 40 >> 2] = 1399285583; - HEAP16[$0 + 44 >> 1] = 0; - $2 = HEAP32[$0 + 16 >> 2]; - $3 = (HEAPU8[$2 + 1 | 0] ^ -1) & 1; - $3 = $11 ? $3 : $3 | 2; - HEAP8[$0 + 45 | 0] = $3; - if (!(!HEAP32[$0 + 328 >> 2] | ($4 | 0) != ($8 | 0))) { - HEAP8[$0 + 45 | 0] = $3 | 4 - } - HEAP32[$0 + 332 >> 2] = 1; - HEAP8[$0 + 53 | 0] = $5 >>> 24; - HEAP8[$0 + 52 | 0] = $5 >>> 16; - HEAP8[$0 + 51 | 0] = $5 >>> 8; - HEAP8[$0 + 50 | 0] = $5; - HEAP8[$0 + 49 | 0] = ($5 & 16777215) << 8 | $7 >>> 24; - HEAP8[$0 + 48 | 0] = ($5 & 65535) << 16 | $7 >>> 16; - HEAP8[$0 + 47 | 0] = ($5 & 255) << 24 | $7 >>> 8; - HEAP8[$0 + 46 | 0] = $7; - $3 = HEAP32[$0 + 336 >> 2]; - HEAP8[$0 + 54 | 0] = $3; - HEAP8[$0 + 55 | 0] = $3 >>> 8; - HEAP8[$0 + 56 | 0] = $3 >>> 16; - HEAP8[$0 + 57 | 0] = $3 >>> 24; - $3 = HEAP32[$0 + 340 >> 2]; - if (($3 | 0) == -1) { - HEAP32[$0 + 340 >> 2] = 0; - $3 = 0; - } - HEAP8[$0 + 66 | 0] = $4; - $6 = 0; - HEAP16[$0 + 62 >> 1] = 0; - HEAP16[$0 + 64 >> 1] = 0; - HEAP8[$0 + 61 | 0] = $3 >>> 24; - HEAP8[$0 + 60 | 0] = $3 >>> 16; - HEAP8[$0 + 59 | 0] = $3 >>> 8; - HEAP8[$0 + 58 | 0] = $3; - $14 = 1; - HEAP32[$0 + 340 >> 2] = $3 + 1; - if (($4 | 0) >= 1) { - $3 = 0; - while (1) { - $5 = HEAP32[$2 + ($3 << 2) >> 2]; - HEAP8[($0 + $3 | 0) + 67 | 0] = $5; - $6 = ($5 & 255) + $6 | 0; - $3 = $3 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - HEAP32[$1 >> 2] = $0 + 40; - $3 = $4 + 27 | 0; - HEAP32[$0 + 324 >> 2] = $3; - HEAP32[$1 + 4 >> 2] = $3; - $3 = HEAP32[$0 + 12 >> 2]; - HEAP32[$1 + 12 >> 2] = $6; - HEAP32[$1 + 8 >> 2] = $3 + $10; - $3 = $8 - $4 | 0; - HEAP32[$0 + 28 >> 2] = $3; - memmove($2, $2 + ($4 << 2) | 0, $3 << 2); - $2 = HEAP32[$0 + 20 >> 2]; - memmove($2, $2 + ($4 << 3) | 0, HEAP32[$0 + 28 >> 2] << 3); - HEAP32[$0 + 12 >> 2] = HEAP32[$0 + 12 >> 2] + $6; - if (!$1) { - break label$1 - } - $0 = 0; - HEAP8[HEAP32[$1 >> 2] + 22 | 0] = 0; - HEAP8[HEAP32[$1 >> 2] + 23 | 0] = 0; - HEAP8[HEAP32[$1 >> 2] + 24 | 0] = 0; - HEAP8[HEAP32[$1 >> 2] + 25 | 0] = 0; - $2 = HEAP32[$1 + 4 >> 2]; - if (($2 | 0) >= 1) { - $4 = HEAP32[$1 >> 2]; - $3 = 0; - while (1) { - $0 = HEAP32[((HEAPU8[$3 + $4 | 0] ^ $0 >>> 24) << 2) + 6512 >> 2] ^ $0 << 8; - $3 = $3 + 1 | 0; - if (($2 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - $2 = HEAP32[$1 + 12 >> 2]; - if (($2 | 0) >= 1) { - $4 = HEAP32[$1 + 8 >> 2]; - $3 = 0; - while (1) { - $0 = HEAP32[((HEAPU8[$3 + $4 | 0] ^ $0 >>> 24) << 2) + 6512 >> 2] ^ $0 << 8; - $3 = $3 + 1 | 0; - if (($2 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - HEAP8[HEAP32[$1 >> 2] + 22 | 0] = $0; - HEAP8[HEAP32[$1 >> 2] + 23 | 0] = $0 >>> 8; - HEAP8[HEAP32[$1 >> 2] + 24 | 0] = $0 >>> 16; - HEAP8[HEAP32[$1 >> 2] + 25 | 0] = $0 >>> 24; - } - return $14; - } - - function ogg_stream_pageout($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - if (!(!$0 | !HEAP32[$0 >> 2])) { - $2 = HEAP32[$0 + 28 >> 2]; - $4 = $0; - label$2 : { - label$3 : { - if (HEAP32[$0 + 328 >> 2]) { - if ($2) { - break label$3 - } - $3 = 0; - break label$2; - } - $3 = 0; - if (HEAP32[$0 + 332 >> 2] | !$2) { - break label$2 - } - } - $3 = 1; - } - $2 = ogg_stream_flush_i($4, $1, $3); - } - return $2; - } - - function ogg_sync_init($0) { - if ($0) { - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - } - return 0; - } - - function ogg_sync_clear($0) { - var $1 = 0; - if ($0) { - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - } - } - - function ogg_sync_buffer($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $2 = HEAP32[$0 + 4 >> 2]; - if (($2 | 0) >= 0) { - $4 = HEAP32[$0 + 12 >> 2]; - if ($4) { - $3 = HEAP32[$0 + 8 >> 2] - $4 | 0; - HEAP32[$0 + 8 >> 2] = $3; - if (($3 | 0) >= 1) { - $2 = HEAP32[$0 >> 2]; - memmove($2, $2 + $4 | 0, $3); - $2 = HEAP32[$0 + 4 >> 2]; - } - HEAP32[$0 + 12 >> 2] = 0; - } - $3 = $2; - $2 = HEAP32[$0 + 8 >> 2]; - label$4 : { - if (($3 - $2 | 0) >= ($1 | 0)) { - $1 = HEAP32[$0 >> 2]; - break label$4; - } - $2 = ($1 + $2 | 0) + 4096 | 0; - $1 = HEAP32[$0 >> 2]; - label$6 : { - if ($1) { - $1 = dlrealloc($1, $2); - break label$6; - } - $1 = dlmalloc($2); - } - if (!$1) { - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - return 0; - } - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 >> 2] = $1; - $2 = HEAP32[$0 + 8 >> 2]; - } - $0 = $1 + $2 | 0; - } else { - $0 = 0 - } - return $0; - } - - function ogg_sync_wrote($0, $1) { - var $2 = 0, $3 = 0; - $2 = -1; - $3 = HEAP32[$0 + 4 >> 2]; - label$1 : { - if (($3 | 0) < 0) { - break label$1 - } - $1 = HEAP32[$0 + 8 >> 2] + $1 | 0; - if (($1 | 0) > ($3 | 0)) { - break label$1 - } - HEAP32[$0 + 8 >> 2] = $1; - $2 = 0; - } - return $2; - } - - function ogg_sync_pageseek($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0; - $9 = global$0 - 16 | 0; - global$0 = $9; - label$1 : { - if (HEAP32[$0 + 4 >> 2] < 0) { - break label$1 - } - $4 = HEAP32[$0 + 12 >> 2]; - $10 = HEAP32[$0 + 8 >> 2] - $4 | 0; - $2 = $4 + HEAP32[$0 >> 2] | 0; - label$2 : { - label$3 : { - label$4 : { - $5 = HEAP32[$0 + 20 >> 2]; - label$5 : { - if (!$5) { - if (($10 | 0) < 27) { - break label$1 - } - if ((HEAPU8[$2 | 0] | HEAPU8[$2 + 1 | 0] << 8 | (HEAPU8[$2 + 2 | 0] << 16 | HEAPU8[$2 + 3 | 0] << 24)) != 1399285583) { - break label$5 - } - $4 = HEAPU8[$2 + 26 | 0]; - $5 = $4 + 27 | 0; - if (($10 | 0) < ($5 | 0)) { - break label$1 - } - if ($4) { - $4 = HEAP32[$0 + 24 >> 2]; - while (1) { - $4 = HEAPU8[($2 + $6 | 0) + 27 | 0] + $4 | 0; - HEAP32[$0 + 24 >> 2] = $4; - $6 = $6 + 1 | 0; - if ($6 >>> 0 < HEAPU8[$2 + 26 | 0]) { - continue - } - break; - }; - } - HEAP32[$0 + 20 >> 2] = $5; - } - if ((HEAP32[$0 + 24 >> 2] + $5 | 0) > ($10 | 0)) { - break label$1 - } - $7 = HEAPU8[$2 + 22 | 0] | HEAPU8[$2 + 23 | 0] << 8 | (HEAPU8[$2 + 24 | 0] << 16 | HEAPU8[$2 + 25 | 0] << 24); - HEAP32[$9 + 12 >> 2] = $7; - $6 = 0; - HEAP8[$2 + 22 | 0] = 0; - HEAP8[$2 + 23 | 0] = 0; - HEAP8[$2 + 24 | 0] = 0; - HEAP8[$2 + 25 | 0] = 0; - $11 = HEAP32[$0 + 24 >> 2]; - $8 = HEAP32[$0 + 20 >> 2]; - HEAP8[$2 + 22 | 0] = 0; - HEAP8[$2 + 23 | 0] = 0; - HEAP8[$2 + 24 | 0] = 0; - HEAP8[$2 + 25 | 0] = 0; - if (($8 | 0) > 0) { - $5 = 0; - while (1) { - $3 = HEAP32[((HEAPU8[$2 + $5 | 0] ^ $3 >>> 24) << 2) + 6512 >> 2] ^ $3 << 8; - $5 = $5 + 1 | 0; - if (($8 | 0) != ($5 | 0)) { - continue - } - break; - }; - } - $4 = $2 + 22 | 0; - if (($11 | 0) > 0) { - $8 = $2 + $8 | 0; - while (1) { - $3 = HEAP32[((HEAPU8[$6 + $8 | 0] ^ $3 >>> 24) << 2) + 6512 >> 2] ^ $3 << 8; - $6 = $6 + 1 | 0; - if (($11 | 0) != ($6 | 0)) { - continue - } - break; - }; - } - HEAP8[$2 + 22 | 0] = $3; - HEAP8[$2 + 23 | 0] = $3 >>> 8; - HEAP8[$2 + 24 | 0] = $3 >>> 16; - HEAP8[$2 + 25 | 0] = $3 >>> 24; - if (HEAP32[$9 + 12 >> 2] == (HEAPU8[$4 | 0] | HEAPU8[$4 + 1 | 0] << 8 | (HEAPU8[$4 + 2 | 0] << 16 | HEAPU8[$4 + 3 | 0] << 24))) { - break label$4 - } - HEAP8[$4 | 0] = $7; - HEAP8[$4 + 1 | 0] = $7 >>> 8; - HEAP8[$4 + 2 | 0] = $7 >>> 16; - HEAP8[$4 + 3 | 0] = $7 >>> 24; - } - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - $3 = memchr($2 + 1 | 0, $10 + -1 | 0); - if (!$3) { - break label$3 - } - $6 = HEAP32[$0 >> 2]; - break label$2; - } - $7 = HEAP32[$0 + 12 >> 2]; - label$13 : { - if (!$1) { - $5 = HEAP32[$0 + 24 >> 2]; - $3 = HEAP32[$0 + 20 >> 2]; - break label$13; - } - $4 = $7 + HEAP32[$0 >> 2] | 0; - HEAP32[$1 >> 2] = $4; - $3 = HEAP32[$0 + 20 >> 2]; - HEAP32[$1 + 4 >> 2] = $3; - HEAP32[$1 + 8 >> 2] = $3 + $4; - $5 = HEAP32[$0 + 24 >> 2]; - HEAP32[$1 + 12 >> 2] = $5; - } - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - $3 = $3 + $5 | 0; - HEAP32[$0 + 12 >> 2] = $7 + $3; - break label$1; - } - $6 = HEAP32[$0 >> 2]; - $3 = $6 + HEAP32[$0 + 8 >> 2] | 0; - } - HEAP32[$0 + 12 >> 2] = $3 - $6; - $3 = $2 - $3 | 0; - } - global$0 = $9 + 16 | 0; - return $3; - } - - function ogg_sync_pageout($0, $1) { - var $2 = 0; - if (HEAP32[$0 + 4 >> 2] >= 0) { - while (1) { - $2 = ogg_sync_pageseek($0, $1); - if (($2 | 0) > 0) { - return 1 - } - if (!$2) { - return 0 - } - if (HEAP32[$0 + 16 >> 2]) { - continue - } - break; - }; - HEAP32[$0 + 16 >> 2] = 1; - $0 = -1; - } else { - $0 = 0 - } - return $0; - } - - function ogg_stream_pagein($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; - $4 = -1; - folding_inner0 : { - label$1 : { - if (!$0) { - break label$1 - } - $6 = HEAP32[$0 >> 2]; - if (!$6) { - break label$1 - } - $3 = HEAP32[$1 >> 2]; - $10 = HEAPU8[$3 + 5 | 0]; - $5 = HEAP32[$1 + 12 >> 2]; - $11 = HEAP32[$1 + 8 >> 2]; - $8 = HEAPU8[$3 + 26 | 0]; - $13 = HEAPU8[$3 + 18 | 0] | HEAPU8[$3 + 19 | 0] << 8 | (HEAPU8[$3 + 20 | 0] << 16 | HEAPU8[$3 + 21 | 0] << 24); - $9 = HEAPU8[$3 + 14 | 0] | HEAPU8[$3 + 15 | 0] << 8 | (HEAPU8[$3 + 16 | 0] << 16 | HEAPU8[$3 + 17 | 0] << 24); - $14 = HEAPU8[$3 + 6 | 0] | HEAPU8[$3 + 7 | 0] << 8 | (HEAPU8[$3 + 8 | 0] << 16 | HEAPU8[$3 + 9 | 0] << 24); - $15 = HEAPU8[$3 + 10 | 0] | HEAPU8[$3 + 11 | 0] << 8 | (HEAPU8[$3 + 12 | 0] << 16 | HEAPU8[$3 + 13 | 0] << 24); - $12 = HEAPU8[$3 + 4 | 0]; - $2 = HEAP32[$0 + 36 >> 2]; - $1 = HEAP32[$0 + 12 >> 2]; - if ($1) { - $7 = HEAP32[$0 + 8 >> 2] - $1 | 0; - HEAP32[$0 + 8 >> 2] = $7; - if ($7) { - memmove($6, $1 + $6 | 0, $7) - } - HEAP32[$0 + 12 >> 2] = 0; - } - if ($2) { - $1 = $0; - $6 = HEAP32[$0 + 28 >> 2] - $2 | 0; - if ($6) { - $7 = HEAP32[$0 + 16 >> 2]; - memmove($7, $7 + ($2 << 2) | 0, $6 << 2); - $6 = HEAP32[$0 + 20 >> 2]; - memmove($6, $6 + ($2 << 3) | 0, HEAP32[$0 + 28 >> 2] - $2 << 3); - $7 = HEAP32[$0 + 28 >> 2] - $2 | 0; - } else { - $7 = 0 - } - HEAP32[$1 + 28 >> 2] = $7; - HEAP32[$0 + 36 >> 2] = 0; - HEAP32[$0 + 32 >> 2] = HEAP32[$0 + 32 >> 2] - $2; - } - if (($9 | 0) != HEAP32[$0 + 336 >> 2] | $12) { - break label$1 - } - if (_os_lacing_expand($0, $8 + 1 | 0)) { - break label$1 - } - $7 = $10 & 1; - $6 = HEAP32[$0 + 340 >> 2]; - label$7 : { - if (($6 | 0) == ($13 | 0)) { - break label$7 - } - $2 = HEAP32[$0 + 32 >> 2]; - $9 = HEAP32[$0 + 28 >> 2]; - if (($2 | 0) < ($9 | 0)) { - $4 = HEAP32[$0 + 8 >> 2]; - $12 = HEAP32[$0 + 16 >> 2]; - $1 = $2; - while (1) { - $4 = $4 - HEAPU8[$12 + ($1 << 2) | 0] | 0; - $1 = $1 + 1 | 0; - if (($1 | 0) < ($9 | 0)) { - continue - } - break; - }; - HEAP32[$0 + 8 >> 2] = $4; - } - HEAP32[$0 + 28 >> 2] = $2; - if (($6 | 0) == -1) { - break label$7 - } - $1 = $2 + 1 | 0; - HEAP32[$0 + 28 >> 2] = $1; - HEAP32[HEAP32[$0 + 16 >> 2] + ($2 << 2) >> 2] = 1024; - HEAP32[$0 + 32 >> 2] = $1; - } - $6 = $10 & 2; - $4 = 0; - label$10 : { - if (!$7) { - break label$10 - } - $1 = HEAP32[$0 + 28 >> 2]; - if (HEAP32[(HEAP32[$0 + 16 >> 2] + ($1 << 2) | 0) + -4 >> 2] != 1024 ? ($1 | 0) >= 1 : 0) { - break label$10 - } - $6 = 0; - if (!$8) { - break label$10 - } - $1 = 0; - while (1) { - $4 = $1 + 1 | 0; - $1 = HEAPU8[($1 + $3 | 0) + 27 | 0]; - $5 = $5 - $1 | 0; - $11 = $1 + $11 | 0; - if (($1 | 0) != 255) { - break label$10 - } - $1 = $4; - if (($8 | 0) != ($1 | 0)) { - continue - } - break; - }; - $4 = $8; - } - if ($5) { - $2 = HEAP32[$0 + 4 >> 2]; - $1 = HEAP32[$0 + 8 >> 2]; - label$15 : { - if (($2 - $5 | 0) > ($1 | 0)) { - $2 = HEAP32[$0 >> 2]; - break label$15; - } - if (($2 | 0) > (2147483647 - $5 | 0)) { - break folding_inner0 - } - $1 = $2 + $5 | 0; - $1 = ($1 | 0) < 2147482623 ? $1 + 1024 | 0 : $1; - $2 = dlrealloc(HEAP32[$0 >> 2], $1); - if (!$2) { - break folding_inner0 - } - HEAP32[$0 >> 2] = $2; - HEAP32[$0 + 4 >> 2] = $1; - $1 = HEAP32[$0 + 8 >> 2]; - } - memcpy($1 + $2 | 0, $11, $5); - HEAP32[$0 + 8 >> 2] = HEAP32[$0 + 8 >> 2] + $5; - } - $11 = $10 & 4; - label$25 : { - if (($4 | 0) >= ($8 | 0)) { - break label$25 - } - $10 = HEAP32[$0 + 20 >> 2]; - $7 = HEAP32[$0 + 16 >> 2]; - $2 = HEAP32[$0 + 28 >> 2]; - $1 = $7 + ($2 << 2) | 0; - $5 = HEAPU8[($3 + $4 | 0) + 27 | 0]; - HEAP32[$1 >> 2] = $5; - $9 = $10 + ($2 << 3) | 0; - HEAP32[$9 >> 2] = -1; - HEAP32[$9 + 4 >> 2] = -1; - if ($6) { - HEAP32[$1 >> 2] = $5 | 256 - } - $1 = $2 + 1 | 0; - HEAP32[$0 + 28 >> 2] = $1; - $4 = $4 + 1 | 0; - label$27 : { - if (($5 | 0) == 255) { - $2 = -1; - break label$27; - } - HEAP32[$0 + 32 >> 2] = $1; - } - if (($4 | 0) != ($8 | 0)) { - while (1) { - $6 = HEAPU8[($3 + $4 | 0) + 27 | 0]; - HEAP32[$7 + ($1 << 2) >> 2] = $6; - $5 = $10 + ($1 << 3) | 0; - HEAP32[$5 >> 2] = -1; - HEAP32[$5 + 4 >> 2] = -1; - $5 = $1 + 1 | 0; - HEAP32[$0 + 28 >> 2] = $5; - $4 = $4 + 1 | 0; - if (($6 | 0) != 255) { - HEAP32[$0 + 32 >> 2] = $5; - $2 = $1; - } - $1 = $5; - if (($4 | 0) != ($8 | 0)) { - continue - } - break; - } - } - if (($2 | 0) == -1) { - break label$25 - } - $1 = HEAP32[$0 + 20 >> 2] + ($2 << 3) | 0; - HEAP32[$1 >> 2] = $14; - HEAP32[$1 + 4 >> 2] = $15; - } - label$32 : { - if (!$11) { - break label$32 - } - HEAP32[$0 + 328 >> 2] = 1; - $1 = HEAP32[$0 + 28 >> 2]; - if (($1 | 0) < 1) { - break label$32 - } - $1 = (HEAP32[$0 + 16 >> 2] + ($1 << 2) | 0) + -4 | 0; - HEAP32[$1 >> 2] = HEAP32[$1 >> 2] | 512; - } - HEAP32[$0 + 340 >> 2] = $13 + 1; - $4 = 0; - } - return $4; - } - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 16 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 20 >> 2]; - if ($1) { - dlfree($1) - } - memset($0, 360); - return -1; - } - - function ogg_sync_reset($0) { - if (HEAP32[$0 + 4 >> 2] < 0) { - return - } - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - } - - function ogg_stream_reset($0) { - if (!$0 | !HEAP32[$0 >> 2]) { - $0 = -1 - } else { - HEAP32[$0 + 344 >> 2] = 0; - HEAP32[$0 + 348 >> 2] = 0; - HEAP32[$0 + 340 >> 2] = -1; - HEAP32[$0 + 332 >> 2] = 0; - HEAP32[$0 + 324 >> 2] = 0; - HEAP32[$0 + 328 >> 2] = 0; - HEAP32[$0 + 36 >> 2] = 0; - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 32 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 + 352 >> 2] = 0; - HEAP32[$0 + 356 >> 2] = 0; - $0 = 0; - } - } - - function ogg_stream_packetout($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - label$1 : { - if (!$0) { - break label$1 - } - $8 = HEAP32[$0 >> 2]; - if (!$8) { - break label$1 - } - $5 = HEAP32[$0 + 36 >> 2]; - if (HEAP32[$0 + 32 >> 2] <= ($5 | 0)) { - break label$1 - } - $3 = HEAP32[$0 + 16 >> 2]; - $6 = HEAP32[$3 + ($5 << 2) >> 2]; - if ($6 & 1024) { - HEAP32[$0 + 36 >> 2] = $5 + 1; - $1 = $0; - $3 = $0; - $2 = HEAP32[$0 + 348 >> 2]; - $0 = HEAP32[$0 + 344 >> 2] + 1 | 0; - if ($0 >>> 0 < 1) { - $2 = $2 + 1 | 0 - } - HEAP32[$3 + 344 >> 2] = $0; - HEAP32[$1 + 348 >> 2] = $2; - return -1; - } - $4 = $6 & 512; - $7 = 255; - $2 = $6 & 255; - label$3 : { - if (($2 | 0) != 255) { - $7 = $2; - break label$3; - } - while (1) { - $5 = $5 + 1 | 0; - $2 = HEAP32[($5 << 2) + $3 >> 2]; - $4 = $2 & 512 ? 512 : $4; - $2 = $2 & 255; - $7 = $2 + $7 | 0; - if (($2 | 0) == 255) { - continue - } - break; - }; - } - label$6 : { - if (!$1) { - $4 = HEAP32[$0 + 344 >> 2]; - $2 = HEAP32[$0 + 348 >> 2]; - $6 = HEAP32[$0 + 12 >> 2]; - break label$6; - } - HEAP32[$1 + 8 >> 2] = $6 & 256; - HEAP32[$1 + 12 >> 2] = $4; - $6 = HEAP32[$0 + 12 >> 2]; - HEAP32[$1 >> 2] = $8 + $6; - $3 = HEAP32[$0 + 348 >> 2]; - $2 = $3; - $4 = HEAP32[$0 + 344 >> 2]; - HEAP32[$1 + 24 >> 2] = $4; - HEAP32[$1 + 28 >> 2] = $2; - $3 = HEAP32[$0 + 20 >> 2] + ($5 << 3) | 0; - $8 = HEAP32[$3 + 4 >> 2]; - $3 = HEAP32[$3 >> 2]; - HEAP32[$1 + 4 >> 2] = $7; - HEAP32[$1 + 16 >> 2] = $3; - HEAP32[$1 + 20 >> 2] = $8; - } - $3 = $4 + 1 | 0; - if ($3 >>> 0 < 1) { - $2 = $2 + 1 | 0 - } - HEAP32[$0 + 344 >> 2] = $3; - HEAP32[$0 + 348 >> 2] = $2; - $4 = 1; - HEAP32[$0 + 36 >> 2] = $5 + 1; - HEAP32[$0 + 12 >> 2] = $6 + $7; - } - return $4; - } - - function FLAC__ogg_decoder_aspect_init($0) { - var $1 = 0; - label$1 : { - if (ogg_stream_init($0 + 8 | 0, HEAP32[$0 + 4 >> 2])) { - break label$1 - } - if (ogg_sync_init($0 + 368 | 0)) { - break label$1 - } - HEAP32[$0 + 396 >> 2] = -1; - HEAP32[$0 + 400 >> 2] = -1; - HEAP32[$0 + 408 >> 2] = 0; - HEAP32[$0 + 412 >> 2] = 0; - HEAP32[$0 + 404 >> 2] = HEAP32[$0 >> 2]; - $1 = 1; - } - return $1; - } - - function FLAC__ogg_decoder_aspect_set_defaults($0) { - HEAP32[$0 >> 2] = 1; - } - - function FLAC__ogg_decoder_aspect_reset($0) { - ogg_stream_reset($0 + 8 | 0); - ogg_sync_reset($0 + 368 | 0); - HEAP32[$0 + 408 >> 2] = 0; - HEAP32[$0 + 412 >> 2] = 0; - if (HEAP32[$0 >> 2]) { - HEAP32[$0 + 404 >> 2] = 1 - } - } - - function FLAC__ogg_decoder_aspect_read_callback_wrapper($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; - $8 = global$0 - 16 | 0; - global$0 = $8; - $9 = HEAP32[$2 >> 2]; - HEAP32[$2 >> 2] = 0; - label$1 : { - label$2 : { - label$3 : { - if (!$9) { - break label$3 - } - $10 = $0 + 416 | 0; - $11 = $0 + 368 | 0; - $13 = $0 + 440 | 0; - $14 = $0 + 8 | 0; - $15 = HEAP32[2721]; - $16 = HEAPU8[7536]; - while (1) { - if (HEAP32[$0 + 408 >> 2]) { - break label$3 - } - label$5 : { - label$6 : { - if (HEAP32[$0 + 412 >> 2]) { - if (HEAP32[$0 + 432 >> 2]) { - $7 = HEAP32[$0 + 440 >> 2]; - $6 = HEAP32[$0 + 444 >> 2]; - $5 = $9 - $5 | 0; - if ($6 >>> 0 > $5 >>> 0) { - break label$6 - } - $1 = memcpy($1, $7, $6); - HEAP32[$2 >> 2] = $6 + HEAP32[$2 >> 2]; - HEAP32[$0 + 432 >> 2] = 0; - $1 = $1 + $6 | 0; - break label$5; - } - $5 = ogg_stream_packetout($14, $13); - if (($5 | 0) >= 1) { - HEAP32[$0 + 432 >> 2] = 1; - $12 = HEAP32[$0 + 444 >> 2]; - if (($12 | 0) < 1) { - break label$5 - } - $6 = HEAP32[$13 >> 2]; - if (HEAPU8[$6 | 0] != ($16 | 0)) { - break label$5 - } - $7 = 3; - if (($12 | 0) < 9) { - break label$1 - } - $5 = $15; - if ((HEAPU8[$6 + 1 | 0] | HEAPU8[$6 + 2 | 0] << 8 | (HEAPU8[$6 + 3 | 0] << 16 | HEAPU8[$6 + 4 | 0] << 24)) != (HEAPU8[$5 | 0] | HEAPU8[$5 + 1 | 0] << 8 | (HEAPU8[$5 + 2 | 0] << 16 | HEAPU8[$5 + 3 | 0] << 24))) { - break label$1 - } - $5 = HEAPU8[$6 + 5 | 0]; - HEAP32[$0 + 396 >> 2] = $5; - HEAP32[$0 + 400 >> 2] = HEAPU8[$6 + 6 | 0]; - if (($5 | 0) != 1) { - $7 = 4; - break label$1; - } - HEAP32[$0 + 444 >> 2] = $12 + -9; - HEAP32[$0 + 440 >> 2] = $6 + 9; - break label$5; - } - if ($5) { - $7 = 2; - break label$1; - } - HEAP32[$0 + 412 >> 2] = 0; - break label$5; - } - $5 = ogg_sync_pageout($11, $10); - if (($5 | 0) >= 1) { - if (HEAP32[$0 + 404 >> 2]) { - $5 = ogg_page_serialno($10); - HEAP32[$0 + 404 >> 2] = 0; - HEAP32[$0 + 344 >> 2] = $5; - HEAP32[$0 + 4 >> 2] = $5; - } - if (ogg_stream_pagein($14, $10)) { - break label$5 - } - HEAP32[$0 + 432 >> 2] = 0; - HEAP32[$0 + 412 >> 2] = 1; - break label$5; - } - if ($5) { - $7 = 2; - break label$1; - } - $5 = $9 - HEAP32[$2 >> 2] | 0; - $5 = $5 >>> 0 > 8192 ? $5 : 8192; - $6 = ogg_sync_buffer($11, $5); - if (!$6) { - $7 = 7; - break label$1; - } - HEAP32[$8 + 12 >> 2] = $5; - label$16 : { - switch ((FUNCTION_TABLE[8]($3, $6, $8 + 12 | 0, $4) | 0) + -1 | 0) { - case 0: - HEAP32[$0 + 408 >> 2] = 1; - break; - case 4: - break label$2; - default: - break label$16; - }; - } - if ((ogg_sync_wrote($11, HEAP32[$8 + 12 >> 2]) | 0) >= 0) { - break label$5 - } - $7 = 6; - break label$1; - } - $1 = memcpy($1, $7, $5); - HEAP32[$2 >> 2] = $5 + HEAP32[$2 >> 2]; - HEAP32[$0 + 440 >> 2] = $5 + HEAP32[$0 + 440 >> 2]; - HEAP32[$0 + 444 >> 2] = HEAP32[$0 + 444 >> 2] - $5; - $1 = $1 + $5 | 0; - } - $5 = HEAP32[$2 >> 2]; - if ($9 >>> 0 > $5 >>> 0) { - continue - } - break; - }; - } - global$0 = $8 + 16 | 0; - return !$5 & HEAP32[$0 + 408 >> 2] != 0; - } - $7 = 5; - } - global$0 = $8 + 16 | 0; - return $7; - } - - function FLAC__MD5Init($0) { - HEAP32[$0 + 80 >> 2] = 0; - HEAP32[$0 + 84 >> 2] = 0; - HEAP32[$0 + 64 >> 2] = 1732584193; - HEAP32[$0 + 68 >> 2] = -271733879; - HEAP32[$0 + 72 >> 2] = -1732584194; - HEAP32[$0 + 76 >> 2] = 271733878; - HEAP32[$0 + 88 >> 2] = 0; - HEAP32[$0 + 92 >> 2] = 0; - } - - function FLAC__MD5Final($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $3 = HEAP32[$1 + 80 >> 2] & 63; - $2 = $3 + $1 | 0; - HEAP8[$2 | 0] = 128; - $2 = $2 + 1 | 0; - $4 = 56; - label$1 : { - if ($3 >>> 0 < 56) { - $4 = 55 - $3 | 0; - break label$1; - } - memset($2, $3 ^ 63); - FLAC__MD5Transform($1 - -64 | 0, $1); - $2 = $1; - } - memset($2, $4); - $2 = HEAP32[$1 + 80 >> 2]; - HEAP32[$1 + 56 >> 2] = $2 << 3; - HEAP32[$1 + 60 >> 2] = HEAP32[$1 + 84 >> 2] << 3 | $2 >>> 29; - FLAC__MD5Transform($1 - -64 | 0, $1); - $2 = HEAPU8[$1 + 76 | 0] | HEAPU8[$1 + 77 | 0] << 8 | (HEAPU8[$1 + 78 | 0] << 16 | HEAPU8[$1 + 79 | 0] << 24); - $3 = HEAPU8[$1 + 72 | 0] | HEAPU8[$1 + 73 | 0] << 8 | (HEAPU8[$1 + 74 | 0] << 16 | HEAPU8[$1 + 75 | 0] << 24); - HEAP8[$0 + 8 | 0] = $3; - HEAP8[$0 + 9 | 0] = $3 >>> 8; - HEAP8[$0 + 10 | 0] = $3 >>> 16; - HEAP8[$0 + 11 | 0] = $3 >>> 24; - HEAP8[$0 + 12 | 0] = $2; - HEAP8[$0 + 13 | 0] = $2 >>> 8; - HEAP8[$0 + 14 | 0] = $2 >>> 16; - HEAP8[$0 + 15 | 0] = $2 >>> 24; - $2 = HEAPU8[$1 + 68 | 0] | HEAPU8[$1 + 69 | 0] << 8 | (HEAPU8[$1 + 70 | 0] << 16 | HEAPU8[$1 + 71 | 0] << 24); - $3 = HEAPU8[$1 + 64 | 0] | HEAPU8[$1 + 65 | 0] << 8 | (HEAPU8[$1 + 66 | 0] << 16 | HEAPU8[$1 + 67 | 0] << 24); - HEAP8[$0 | 0] = $3; - HEAP8[$0 + 1 | 0] = $3 >>> 8; - HEAP8[$0 + 2 | 0] = $3 >>> 16; - HEAP8[$0 + 3 | 0] = $3 >>> 24; - HEAP8[$0 + 4 | 0] = $2; - HEAP8[$0 + 5 | 0] = $2 >>> 8; - HEAP8[$0 + 6 | 0] = $2 >>> 16; - HEAP8[$0 + 7 | 0] = $2 >>> 24; - $0 = HEAP32[$1 + 88 >> 2]; - if ($0) { - dlfree($0); - HEAP32[$1 + 88 >> 2] = 0; - HEAP32[$1 + 92 >> 2] = 0; - } - memset($1, 96); - } - - function FLAC__MD5Transform($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $11 = HEAP32[$1 + 16 >> 2]; - $8 = HEAP32[$1 + 32 >> 2]; - $12 = HEAP32[$1 + 48 >> 2]; - $13 = HEAP32[$1 + 36 >> 2]; - $14 = HEAP32[$1 + 52 >> 2]; - $15 = HEAP32[$1 + 4 >> 2]; - $6 = HEAP32[$1 + 20 >> 2]; - $7 = HEAP32[$0 + 4 >> 2]; - $9 = HEAP32[$1 >> 2]; - $25 = HEAP32[$0 >> 2]; - $16 = HEAP32[$0 + 12 >> 2]; - $10 = HEAP32[$0 + 8 >> 2]; - $3 = $7 + __wasm_rotl_i32((($9 + $25 | 0) + ($16 ^ ($16 ^ $10) & $7) | 0) + -680876936 | 0, 7) | 0; - $17 = HEAP32[$1 + 12 >> 2]; - $18 = HEAP32[$1 + 8 >> 2]; - $4 = __wasm_rotl_i32((($15 + $16 | 0) + ($3 & ($7 ^ $10) ^ $10) | 0) + -389564586 | 0, 12) + $3 | 0; - $2 = __wasm_rotl_i32((($18 + $10 | 0) + ($4 & ($3 ^ $7) ^ $7) | 0) + 606105819 | 0, 17) + $4 | 0; - $5 = __wasm_rotl_i32((($7 + $17 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -1044525330 | 0, 22) + $2 | 0; - $3 = __wasm_rotl_i32((($3 + $11 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + -176418897 | 0, 7) + $5 | 0; - $19 = HEAP32[$1 + 28 >> 2]; - $20 = HEAP32[$1 + 24 >> 2]; - $4 = __wasm_rotl_i32((($4 + $6 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + 1200080426 | 0, 12) + $3 | 0; - $2 = __wasm_rotl_i32((($2 + $20 | 0) + ($5 ^ $4 & ($3 ^ $5)) | 0) + -1473231341 | 0, 17) + $4 | 0; - $5 = __wasm_rotl_i32((($5 + $19 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -45705983 | 0, 22) + $2 | 0; - $3 = __wasm_rotl_i32((($3 + $8 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + 1770035416 | 0, 7) + $5 | 0; - $21 = HEAP32[$1 + 44 >> 2]; - $22 = HEAP32[$1 + 40 >> 2]; - $4 = __wasm_rotl_i32((($4 + $13 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + -1958414417 | 0, 12) + $3 | 0; - $2 = __wasm_rotl_i32((($2 + $22 | 0) + ($5 ^ $4 & ($3 ^ $5)) | 0) + -42063 | 0, 17) + $4 | 0; - $5 = __wasm_rotl_i32((($5 + $21 | 0) + ($3 ^ $2 & ($3 ^ $4)) | 0) + -1990404162 | 0, 22) + $2 | 0; - $3 = __wasm_rotl_i32((($3 + $12 | 0) + ($4 ^ $5 & ($2 ^ $4)) | 0) + 1804603682 | 0, 7) + $5 | 0; - $23 = HEAP32[$1 + 56 >> 2]; - $24 = HEAP32[$1 + 60 >> 2]; - $4 = __wasm_rotl_i32((($4 + $14 | 0) + ($2 ^ $3 & ($2 ^ $5)) | 0) + -40341101 | 0, 12) + $3 | 0; - $1 = $4 + __wasm_rotl_i32((($2 + $23 | 0) + ($5 ^ ($3 ^ $5) & $4) | 0) + -1502002290 | 0, 17) | 0; - $26 = $1 + $21 | 0; - $2 = $3 + $15 | 0; - $3 = __wasm_rotl_i32((($5 + $24 | 0) + ($3 ^ $1 & ($3 ^ $4)) | 0) + 1236535329 | 0, 22) + $1 | 0; - $2 = __wasm_rotl_i32(($2 + ($1 ^ ($3 ^ $1) & $4) | 0) + -165796510 | 0, 5) + $3 | 0; - $1 = __wasm_rotl_i32((($4 + $20 | 0) + ($3 ^ $1 & ($3 ^ $2)) | 0) + -1069501632 | 0, 9) + $2 | 0; - $4 = __wasm_rotl_i32(($26 + (($2 ^ $1) & $3 ^ $2) | 0) + 643717713 | 0, 14) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $9 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + -373897302 | 0, 20) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $6 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + -701558691 | 0, 5) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $22 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + 38016083 | 0, 9) + $2 | 0; - $4 = __wasm_rotl_i32((($24 + $4 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + -660478335 | 0, 14) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $11 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + -405537848 | 0, 20) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $13 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + 568446438 | 0, 5) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $23 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + -1019803690 | 0, 9) + $2 | 0; - $4 = __wasm_rotl_i32((($4 + $17 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + -187363961 | 0, 14) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $8 | 0) + ($1 ^ $2 & ($1 ^ $4)) | 0) + 1163531501 | 0, 20) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($4 ^ $1 & ($3 ^ $4)) | 0) + -1444681467 | 0, 5) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $18 | 0) + ($3 ^ $4 & ($3 ^ $2)) | 0) + -51403784 | 0, 9) + $2 | 0; - $4 = __wasm_rotl_i32((($4 + $19 | 0) + (($2 ^ $1) & $3 ^ $2) | 0) + 1735328473 | 0, 14) + $1 | 0; - $5 = $1 ^ $4; - $3 = __wasm_rotl_i32((($3 + $12 | 0) + ($1 ^ $5 & $2) | 0) + -1926607734 | 0, 20) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $6 | 0) + ($3 ^ $5) | 0) + -378558 | 0, 4) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $8 | 0) + ($3 ^ $4 ^ $2) | 0) + -2022574463 | 0, 11) + $2 | 0; - $4 = __wasm_rotl_i32((($4 + $21 | 0) + ($1 ^ ($3 ^ $2)) | 0) + 1839030562 | 0, 16) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $23 | 0) + ($4 ^ ($1 ^ $2)) | 0) + -35309556 | 0, 23) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $15 | 0) + ($3 ^ ($1 ^ $4)) | 0) + -1530992060 | 0, 4) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $11 | 0) + ($2 ^ ($3 ^ $4)) | 0) + 1272893353 | 0, 11) + $2 | 0; - $4 = __wasm_rotl_i32((($4 + $19 | 0) + ($1 ^ ($3 ^ $2)) | 0) + -155497632 | 0, 16) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $22 | 0) + ($4 ^ ($1 ^ $2)) | 0) + -1094730640 | 0, 23) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($3 ^ ($1 ^ $4)) | 0) + 681279174 | 0, 4) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $9 | 0) + ($2 ^ ($3 ^ $4)) | 0) + -358537222 | 0, 11) + $2 | 0; - $4 = __wasm_rotl_i32((($4 + $17 | 0) + ($1 ^ ($3 ^ $2)) | 0) + -722521979 | 0, 16) + $1 | 0; - $3 = __wasm_rotl_i32((($3 + $20 | 0) + ($4 ^ ($1 ^ $2)) | 0) + 76029189 | 0, 23) + $4 | 0; - $2 = __wasm_rotl_i32((($2 + $13 | 0) + ($3 ^ ($1 ^ $4)) | 0) + -640364487 | 0, 4) + $3 | 0; - $1 = __wasm_rotl_i32((($1 + $12 | 0) + ($2 ^ ($3 ^ $4)) | 0) + -421815835 | 0, 11) + $2 | 0; - $5 = $2 + $9 | 0; - $9 = $1 ^ $2; - $2 = __wasm_rotl_i32((($4 + $24 | 0) + ($1 ^ ($3 ^ $2)) | 0) + 530742520 | 0, 16) + $1 | 0; - $4 = __wasm_rotl_i32((($3 + $18 | 0) + ($9 ^ $2) | 0) + -995338651 | 0, 23) + $2 | 0; - $3 = __wasm_rotl_i32(($5 + (($4 | $1 ^ -1) ^ $2) | 0) + -198630844 | 0, 6) + $4 | 0; - $5 = $4 + $6 | 0; - $6 = $2 + $23 | 0; - $2 = __wasm_rotl_i32((($1 + $19 | 0) + ($4 ^ ($3 | $2 ^ -1)) | 0) + 1126891415 | 0, 10) + $3 | 0; - $4 = __wasm_rotl_i32(($6 + ($3 ^ ($2 | $4 ^ -1)) | 0) + -1416354905 | 0, 15) + $2 | 0; - $1 = __wasm_rotl_i32(($5 + (($4 | $3 ^ -1) ^ $2) | 0) + -57434055 | 0, 21) + $4 | 0; - $5 = $4 + $22 | 0; - $6 = $2 + $17 | 0; - $2 = __wasm_rotl_i32((($3 + $12 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + 1700485571 | 0, 6) + $1 | 0; - $4 = __wasm_rotl_i32(($6 + ($1 ^ ($2 | $4 ^ -1)) | 0) + -1894986606 | 0, 10) + $2 | 0; - $3 = __wasm_rotl_i32(($5 + (($4 | $1 ^ -1) ^ $2) | 0) + -1051523 | 0, 15) + $4 | 0; - $5 = $4 + $24 | 0; - $8 = $2 + $8 | 0; - $2 = __wasm_rotl_i32((($1 + $15 | 0) + ($4 ^ ($3 | $2 ^ -1)) | 0) + -2054922799 | 0, 21) + $3 | 0; - $4 = __wasm_rotl_i32(($8 + ($3 ^ ($2 | $4 ^ -1)) | 0) + 1873313359 | 0, 6) + $2 | 0; - $1 = __wasm_rotl_i32(($5 + (($4 | $3 ^ -1) ^ $2) | 0) + -30611744 | 0, 10) + $4 | 0; - $3 = __wasm_rotl_i32((($3 + $20 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + -1560198380 | 0, 15) + $1 | 0; - $2 = __wasm_rotl_i32((($2 + $14 | 0) + ($1 ^ ($3 | $4 ^ -1)) | 0) + 1309151649 | 0, 21) + $3 | 0; - $4 = __wasm_rotl_i32((($4 + $11 | 0) + (($2 | $1 ^ -1) ^ $3) | 0) + -145523070 | 0, 6) + $2 | 0; - HEAP32[$0 >> 2] = $4 + $25; - $1 = __wasm_rotl_i32((($1 + $21 | 0) + ($2 ^ ($4 | $3 ^ -1)) | 0) + -1120210379 | 0, 10) + $4 | 0; - HEAP32[$0 + 12 >> 2] = $1 + $16; - $3 = __wasm_rotl_i32((($3 + $18 | 0) + ($4 ^ ($1 | $2 ^ -1)) | 0) + 718787259 | 0, 15) + $1 | 0; - HEAP32[$0 + 8 >> 2] = $3 + $10; - (wasm2js_i32$0 = $0, wasm2js_i32$1 = __wasm_rotl_i32((($2 + $13 | 0) + ($1 ^ ($3 | $4 ^ -1)) | 0) + -343485551 | 0, 21) + ($3 + $7 | 0) | 0), HEAP32[wasm2js_i32$0 + 4 >> 2] = wasm2js_i32$1; - } - - function FLAC__MD5Accumulate($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; - __wasm_i64_mul($4, 0, $2, 0); - label$1 : { - if (i64toi32_i32$HIGH_BITS) { - break label$1 - } - $7 = Math_imul($2, $4); - __wasm_i64_mul($3, 0, $7, 0); - if (i64toi32_i32$HIGH_BITS) { - break label$1 - } - $6 = HEAP32[$0 + 88 >> 2]; - $11 = Math_imul($3, $7); - label$2 : { - if (HEAPU32[$0 + 92 >> 2] >= $11 >>> 0) { - $5 = $6; - break label$2; - } - $5 = dlrealloc($6, $11); - label$4 : { - if (!$5) { - dlfree($6); - $5 = dlmalloc($11); - HEAP32[$0 + 88 >> 2] = $5; - if ($5) { - break label$4 - } - HEAP32[$0 + 92 >> 2] = 0; - return 0; - } - HEAP32[$0 + 88 >> 2] = $5; - } - HEAP32[$0 + 92 >> 2] = $11; - } - label$6 : { - label$7 : { - label$8 : { - label$9 : { - label$10 : { - label$11 : { - label$12 : { - label$13 : { - label$14 : { - label$15 : { - label$16 : { - label$17 : { - $6 = Math_imul($4, 100) + $2 | 0; - if (($6 | 0) <= 300) { - label$19 : { - switch ($6 + -101 | 0) { - case 3: - break label$10; - case 5: - break label$11; - case 7: - break label$12; - case 2: - case 4: - case 6: - break label$7; - case 0: - break label$8; - case 1: - break label$9; - default: - break label$19; - }; - } - switch ($6 + -201 | 0) { - case 0: - break label$13; - case 1: - break label$14; - case 3: - break label$15; - case 5: - break label$16; - case 7: - break label$17; - default: - break label$7; - }; - } - label$20 : { - label$21 : { - label$22 : { - switch ($6 + -401 | 0) { - default: - switch ($6 + -301 | 0) { - case 0: - break label$20; - case 1: - break label$21; - default: - break label$7; - }; - case 7: - if (!$3) { - break label$6 - } - $13 = HEAP32[$1 + 28 >> 2]; - $8 = HEAP32[$1 + 24 >> 2]; - $12 = HEAP32[$1 + 20 >> 2]; - $7 = HEAP32[$1 + 16 >> 2]; - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $9 = $2 << 2; - HEAP32[$5 >> 2] = HEAP32[$9 + $1 >> 2]; - HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $9 >> 2]; - HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $9 >> 2]; - HEAP32[$5 + 12 >> 2] = HEAP32[$10 + $9 >> 2]; - HEAP32[$5 + 16 >> 2] = HEAP32[$7 + $9 >> 2]; - HEAP32[$5 + 20 >> 2] = HEAP32[$9 + $12 >> 2]; - HEAP32[$5 + 24 >> 2] = HEAP32[$8 + $9 >> 2]; - HEAP32[$5 + 28 >> 2] = HEAP32[$9 + $13 >> 2]; - $5 = $5 + 32 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - case 5: - if (!$3) { - break label$6 - } - $12 = HEAP32[$1 + 20 >> 2]; - $7 = HEAP32[$1 + 16 >> 2]; - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $8 = $2 << 2; - HEAP32[$5 >> 2] = HEAP32[$8 + $1 >> 2]; - HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $8 >> 2]; - HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $8 >> 2]; - HEAP32[$5 + 12 >> 2] = HEAP32[$8 + $10 >> 2]; - HEAP32[$5 + 16 >> 2] = HEAP32[$7 + $8 >> 2]; - HEAP32[$5 + 20 >> 2] = HEAP32[$8 + $12 >> 2]; - $5 = $5 + 24 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - case 3: - if (!$3) { - break label$6 - } - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $7 = $2 << 2; - HEAP32[$5 >> 2] = HEAP32[$7 + $1 >> 2]; - HEAP32[$5 + 4 >> 2] = HEAP32[$4 + $7 >> 2]; - HEAP32[$5 + 8 >> 2] = HEAP32[$6 + $7 >> 2]; - HEAP32[$5 + 12 >> 2] = HEAP32[$7 + $10 >> 2]; - $5 = $5 + 16 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - case 1: - if (!$3) { - break label$6 - } - $6 = HEAP32[$1 + 4 >> 2]; - $4 = HEAP32[$1 >> 2]; - $1 = 0; - while (1) { - $2 = $1 << 2; - HEAP32[$5 >> 2] = HEAP32[$2 + $4 >> 2]; - HEAP32[$5 + 4 >> 2] = HEAP32[$2 + $6 >> 2]; - $5 = $5 + 8 | 0; - $1 = $1 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$6; - case 0: - break label$22; - case 2: - case 4: - case 6: - break label$7; - }; - } - if (!$3) { - break label$6 - } - $2 = HEAP32[$1 >> 2]; - $1 = 0; - while (1) { - HEAP32[$5 >> 2] = HEAP32[$2 + ($1 << 2) >> 2]; - $5 = $5 + 4 | 0; - $1 = $1 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $2 = 0; - while (1) { - $4 = $2 << 2; - $6 = HEAP32[$4 + HEAP32[$1 >> 2] >> 2]; - HEAP8[$5 | 0] = $6; - HEAP8[$5 + 2 | 0] = $6 >>> 16; - HEAP8[$5 + 1 | 0] = $6 >>> 8; - $4 = HEAP32[$4 + HEAP32[$1 + 4 >> 2] >> 2]; - HEAP8[$5 + 3 | 0] = $4; - HEAP8[$5 + 5 | 0] = $4 >>> 16; - HEAP8[$5 + 4 | 0] = $4 >>> 8; - $5 = $5 + 6 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $2 = 0; - while (1) { - $4 = HEAP32[HEAP32[$1 >> 2] + ($2 << 2) >> 2]; - HEAP8[$5 | 0] = $4; - HEAP8[$5 + 2 | 0] = $4 >>> 16; - HEAP8[$5 + 1 | 0] = $4 >>> 8; - $5 = $5 + 3 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $13 = HEAP32[$1 + 28 >> 2]; - $8 = HEAP32[$1 + 24 >> 2]; - $12 = HEAP32[$1 + 20 >> 2]; - $7 = HEAP32[$1 + 16 >> 2]; - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $9 = $2 << 2; - HEAP16[$5 >> 1] = HEAP32[$9 + $1 >> 2]; - HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $9 >> 2]; - HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $9 >> 2]; - HEAP16[$5 + 6 >> 1] = HEAP32[$10 + $9 >> 2]; - HEAP16[$5 + 8 >> 1] = HEAP32[$7 + $9 >> 2]; - HEAP16[$5 + 10 >> 1] = HEAP32[$9 + $12 >> 2]; - HEAP16[$5 + 12 >> 1] = HEAP32[$8 + $9 >> 2]; - HEAP16[$5 + 14 >> 1] = HEAP32[$9 + $13 >> 2]; - $5 = $5 + 16 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $12 = HEAP32[$1 + 20 >> 2]; - $7 = HEAP32[$1 + 16 >> 2]; - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $8 = $2 << 2; - HEAP16[$5 >> 1] = HEAP32[$8 + $1 >> 2]; - HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $8 >> 2]; - HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $8 >> 2]; - HEAP16[$5 + 6 >> 1] = HEAP32[$8 + $10 >> 2]; - HEAP16[$5 + 8 >> 1] = HEAP32[$7 + $8 >> 2]; - HEAP16[$5 + 10 >> 1] = HEAP32[$8 + $12 >> 2]; - $5 = $5 + 12 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $10 = HEAP32[$1 + 12 >> 2]; - $6 = HEAP32[$1 + 8 >> 2]; - $4 = HEAP32[$1 + 4 >> 2]; - $1 = HEAP32[$1 >> 2]; - $2 = 0; - while (1) { - $7 = $2 << 2; - HEAP16[$5 >> 1] = HEAP32[$7 + $1 >> 2]; - HEAP16[$5 + 2 >> 1] = HEAP32[$4 + $7 >> 2]; - HEAP16[$5 + 4 >> 1] = HEAP32[$6 + $7 >> 2]; - HEAP16[$5 + 6 >> 1] = HEAP32[$7 + $10 >> 2]; - $5 = $5 + 8 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $6 = HEAP32[$1 + 4 >> 2]; - $4 = HEAP32[$1 >> 2]; - $1 = 0; - while (1) { - $2 = $1 << 2; - HEAP16[$5 >> 1] = HEAP32[$2 + $4 >> 2]; - HEAP16[$5 + 2 >> 1] = HEAP32[$2 + $6 >> 2]; - $5 = $5 + 4 | 0; - $1 = $1 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $2 = HEAP32[$1 >> 2]; - $1 = 0; - while (1) { - HEAP16[$5 >> 1] = HEAP32[$2 + ($1 << 2) >> 2]; - $5 = $5 + 2 | 0; - $1 = $1 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $4 = 0; - while (1) { - $2 = $4 << 2; - HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; - HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; - HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; - HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; - HEAP8[$5 + 4 | 0] = HEAP32[$2 + HEAP32[$1 + 16 >> 2] >> 2]; - HEAP8[$5 + 5 | 0] = HEAP32[$2 + HEAP32[$1 + 20 >> 2] >> 2]; - HEAP8[$5 + 6 | 0] = HEAP32[$2 + HEAP32[$1 + 24 >> 2] >> 2]; - HEAP8[$5 + 7 | 0] = HEAP32[$2 + HEAP32[$1 + 28 >> 2] >> 2]; - $5 = $5 + 8 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $4 = 0; - while (1) { - $2 = $4 << 2; - HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; - HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; - HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; - HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; - HEAP8[$5 + 4 | 0] = HEAP32[$2 + HEAP32[$1 + 16 >> 2] >> 2]; - HEAP8[$5 + 5 | 0] = HEAP32[$2 + HEAP32[$1 + 20 >> 2] >> 2]; - $5 = $5 + 6 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $4 = 0; - while (1) { - $2 = $4 << 2; - HEAP8[$5 | 0] = HEAP32[$2 + HEAP32[$1 >> 2] >> 2]; - HEAP8[$5 + 1 | 0] = HEAP32[$2 + HEAP32[$1 + 4 >> 2] >> 2]; - HEAP8[$5 + 2 | 0] = HEAP32[$2 + HEAP32[$1 + 8 >> 2] >> 2]; - HEAP8[$5 + 3 | 0] = HEAP32[$2 + HEAP32[$1 + 12 >> 2] >> 2]; - $5 = $5 + 4 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $2 = 0; - while (1) { - $4 = $2 << 2; - HEAP8[$5 | 0] = HEAP32[$4 + HEAP32[$1 >> 2] >> 2]; - HEAP8[$5 + 1 | 0] = HEAP32[$4 + HEAP32[$1 + 4 >> 2] >> 2]; - $5 = $5 + 2 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - if (!$3) { - break label$6 - } - $2 = 0; - while (1) { - HEAP8[$5 | 0] = HEAP32[HEAP32[$1 >> 2] + ($2 << 2) >> 2]; - $5 = $5 + 1 | 0; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - break label$6; - } - label$45 : { - switch ($4 + -1 | 0) { - case 3: - if (!$2 | !$3) { - break label$6 - } - $6 = 0; - while (1) { - $4 = 0; - while (1) { - HEAP32[$5 >> 2] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; - $5 = $5 + 4 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - $6 = $6 + 1 | 0; - if (($6 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - case 2: - if (!$2 | !$3) { - break label$6 - } - while (1) { - $4 = 0; - while (1) { - $6 = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($10 << 2) >> 2]; - HEAP8[$5 | 0] = $6; - HEAP8[$5 + 2 | 0] = $6 >>> 16; - HEAP8[$5 + 1 | 0] = $6 >>> 8; - $5 = $5 + 3 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - $10 = $10 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - case 1: - if (!$2 | !$3) { - break label$6 - } - $6 = 0; - while (1) { - $4 = 0; - while (1) { - HEAP16[$5 >> 1] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; - $5 = $5 + 2 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - $6 = $6 + 1 | 0; - if (($6 | 0) != ($3 | 0)) { - continue - } - break; - }; - break label$6; - case 0: - break label$45; - default: - break label$6; - }; - } - if (!$2 | !$3) { - break label$6 - } - $6 = 0; - while (1) { - $4 = 0; - while (1) { - HEAP8[$5 | 0] = HEAP32[HEAP32[($4 << 2) + $1 >> 2] + ($6 << 2) >> 2]; - $5 = $5 + 1 | 0; - $4 = $4 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - $6 = $6 + 1 | 0; - if (($6 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - $2 = HEAP32[$0 + 80 >> 2]; - $1 = $2 + $11 | 0; - HEAP32[$0 + 80 >> 2] = $1; - $3 = HEAP32[$0 + 88 >> 2]; - if ($1 >>> 0 < $2 >>> 0) { - $1 = $0 + 84 | 0; - HEAP32[$1 >> 2] = HEAP32[$1 >> 2] + 1; - } - $4 = 64 - ($2 & 63) | 0; - $1 = ($0 - $4 | 0) - -64 | 0; - label$58 : { - if ($11 >>> 0 < $4 >>> 0) { - memcpy($1, $3, $11); - break label$58; - } - memcpy($1, $3, $4); - $2 = $0 - -64 | 0; - FLAC__MD5Transform($2, $0); - $5 = $3 + $4 | 0; - $1 = $11 - $4 | 0; - if ($1 >>> 0 >= 64) { - while (1) { - $4 = HEAPU8[$5 + 4 | 0] | HEAPU8[$5 + 5 | 0] << 8 | (HEAPU8[$5 + 6 | 0] << 16 | HEAPU8[$5 + 7 | 0] << 24); - $3 = HEAPU8[$5 | 0] | HEAPU8[$5 + 1 | 0] << 8 | (HEAPU8[$5 + 2 | 0] << 16 | HEAPU8[$5 + 3 | 0] << 24); - HEAP8[$0 | 0] = $3; - HEAP8[$0 + 1 | 0] = $3 >>> 8; - HEAP8[$0 + 2 | 0] = $3 >>> 16; - HEAP8[$0 + 3 | 0] = $3 >>> 24; - HEAP8[$0 + 4 | 0] = $4; - HEAP8[$0 + 5 | 0] = $4 >>> 8; - HEAP8[$0 + 6 | 0] = $4 >>> 16; - HEAP8[$0 + 7 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 60 | 0] | HEAPU8[$5 + 61 | 0] << 8 | (HEAPU8[$5 + 62 | 0] << 16 | HEAPU8[$5 + 63 | 0] << 24); - $3 = HEAPU8[$5 + 56 | 0] | HEAPU8[$5 + 57 | 0] << 8 | (HEAPU8[$5 + 58 | 0] << 16 | HEAPU8[$5 + 59 | 0] << 24); - HEAP8[$0 + 56 | 0] = $3; - HEAP8[$0 + 57 | 0] = $3 >>> 8; - HEAP8[$0 + 58 | 0] = $3 >>> 16; - HEAP8[$0 + 59 | 0] = $3 >>> 24; - HEAP8[$0 + 60 | 0] = $4; - HEAP8[$0 + 61 | 0] = $4 >>> 8; - HEAP8[$0 + 62 | 0] = $4 >>> 16; - HEAP8[$0 + 63 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 52 | 0] | HEAPU8[$5 + 53 | 0] << 8 | (HEAPU8[$5 + 54 | 0] << 16 | HEAPU8[$5 + 55 | 0] << 24); - $3 = HEAPU8[$5 + 48 | 0] | HEAPU8[$5 + 49 | 0] << 8 | (HEAPU8[$5 + 50 | 0] << 16 | HEAPU8[$5 + 51 | 0] << 24); - HEAP8[$0 + 48 | 0] = $3; - HEAP8[$0 + 49 | 0] = $3 >>> 8; - HEAP8[$0 + 50 | 0] = $3 >>> 16; - HEAP8[$0 + 51 | 0] = $3 >>> 24; - HEAP8[$0 + 52 | 0] = $4; - HEAP8[$0 + 53 | 0] = $4 >>> 8; - HEAP8[$0 + 54 | 0] = $4 >>> 16; - HEAP8[$0 + 55 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 44 | 0] | HEAPU8[$5 + 45 | 0] << 8 | (HEAPU8[$5 + 46 | 0] << 16 | HEAPU8[$5 + 47 | 0] << 24); - $3 = HEAPU8[$5 + 40 | 0] | HEAPU8[$5 + 41 | 0] << 8 | (HEAPU8[$5 + 42 | 0] << 16 | HEAPU8[$5 + 43 | 0] << 24); - HEAP8[$0 + 40 | 0] = $3; - HEAP8[$0 + 41 | 0] = $3 >>> 8; - HEAP8[$0 + 42 | 0] = $3 >>> 16; - HEAP8[$0 + 43 | 0] = $3 >>> 24; - HEAP8[$0 + 44 | 0] = $4; - HEAP8[$0 + 45 | 0] = $4 >>> 8; - HEAP8[$0 + 46 | 0] = $4 >>> 16; - HEAP8[$0 + 47 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 36 | 0] | HEAPU8[$5 + 37 | 0] << 8 | (HEAPU8[$5 + 38 | 0] << 16 | HEAPU8[$5 + 39 | 0] << 24); - $3 = HEAPU8[$5 + 32 | 0] | HEAPU8[$5 + 33 | 0] << 8 | (HEAPU8[$5 + 34 | 0] << 16 | HEAPU8[$5 + 35 | 0] << 24); - HEAP8[$0 + 32 | 0] = $3; - HEAP8[$0 + 33 | 0] = $3 >>> 8; - HEAP8[$0 + 34 | 0] = $3 >>> 16; - HEAP8[$0 + 35 | 0] = $3 >>> 24; - HEAP8[$0 + 36 | 0] = $4; - HEAP8[$0 + 37 | 0] = $4 >>> 8; - HEAP8[$0 + 38 | 0] = $4 >>> 16; - HEAP8[$0 + 39 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 28 | 0] | HEAPU8[$5 + 29 | 0] << 8 | (HEAPU8[$5 + 30 | 0] << 16 | HEAPU8[$5 + 31 | 0] << 24); - $3 = HEAPU8[$5 + 24 | 0] | HEAPU8[$5 + 25 | 0] << 8 | (HEAPU8[$5 + 26 | 0] << 16 | HEAPU8[$5 + 27 | 0] << 24); - HEAP8[$0 + 24 | 0] = $3; - HEAP8[$0 + 25 | 0] = $3 >>> 8; - HEAP8[$0 + 26 | 0] = $3 >>> 16; - HEAP8[$0 + 27 | 0] = $3 >>> 24; - HEAP8[$0 + 28 | 0] = $4; - HEAP8[$0 + 29 | 0] = $4 >>> 8; - HEAP8[$0 + 30 | 0] = $4 >>> 16; - HEAP8[$0 + 31 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 20 | 0] | HEAPU8[$5 + 21 | 0] << 8 | (HEAPU8[$5 + 22 | 0] << 16 | HEAPU8[$5 + 23 | 0] << 24); - $3 = HEAPU8[$5 + 16 | 0] | HEAPU8[$5 + 17 | 0] << 8 | (HEAPU8[$5 + 18 | 0] << 16 | HEAPU8[$5 + 19 | 0] << 24); - HEAP8[$0 + 16 | 0] = $3; - HEAP8[$0 + 17 | 0] = $3 >>> 8; - HEAP8[$0 + 18 | 0] = $3 >>> 16; - HEAP8[$0 + 19 | 0] = $3 >>> 24; - HEAP8[$0 + 20 | 0] = $4; - HEAP8[$0 + 21 | 0] = $4 >>> 8; - HEAP8[$0 + 22 | 0] = $4 >>> 16; - HEAP8[$0 + 23 | 0] = $4 >>> 24; - $4 = HEAPU8[$5 + 12 | 0] | HEAPU8[$5 + 13 | 0] << 8 | (HEAPU8[$5 + 14 | 0] << 16 | HEAPU8[$5 + 15 | 0] << 24); - $3 = HEAPU8[$5 + 8 | 0] | HEAPU8[$5 + 9 | 0] << 8 | (HEAPU8[$5 + 10 | 0] << 16 | HEAPU8[$5 + 11 | 0] << 24); - HEAP8[$0 + 8 | 0] = $3; - HEAP8[$0 + 9 | 0] = $3 >>> 8; - HEAP8[$0 + 10 | 0] = $3 >>> 16; - HEAP8[$0 + 11 | 0] = $3 >>> 24; - HEAP8[$0 + 12 | 0] = $4; - HEAP8[$0 + 13 | 0] = $4 >>> 8; - HEAP8[$0 + 14 | 0] = $4 >>> 16; - HEAP8[$0 + 15 | 0] = $4 >>> 24; - FLAC__MD5Transform($2, $0); - $5 = $5 - -64 | 0; - $1 = $1 + -64 | 0; - if ($1 >>> 0 > 63) { - continue - } - break; - } - } - memcpy($0, $5, $1); - } - $5 = 1; - } - return $5; - } - - function __stdio_close($0) { - $0 = $0 | 0; - return __wasi_fd_close(HEAP32[$0 + 60 >> 2]) | 0; - } - - function __wasi_syscall_ret($0) { - if (!$0) { - return 0 - } - HEAP32[2896] = $0; - return -1; - } - - function __stdio_read($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0, $5 = 0, $6 = 0; - $3 = global$0 - 32 | 0; - global$0 = $3; - HEAP32[$3 + 16 >> 2] = $1; - $4 = HEAP32[$0 + 48 >> 2]; - HEAP32[$3 + 20 >> 2] = $2 - (($4 | 0) != 0); - $5 = HEAP32[$0 + 44 >> 2]; - HEAP32[$3 + 28 >> 2] = $4; - HEAP32[$3 + 24 >> 2] = $5; - label$1 : { - label$2 : { - label$3 : { - if (__wasi_syscall_ret(__wasi_fd_read(HEAP32[$0 + 60 >> 2], $3 + 16 | 0, 2, $3 + 12 | 0) | 0)) { - HEAP32[$3 + 12 >> 2] = -1; - $2 = -1; - break label$3; - } - $4 = HEAP32[$3 + 12 >> 2]; - if (($4 | 0) > 0) { - break label$2 - } - $2 = $4; - } - HEAP32[$0 >> 2] = HEAP32[$0 >> 2] | $2 & 48 ^ 16; - break label$1; - } - $6 = HEAP32[$3 + 20 >> 2]; - if ($4 >>> 0 <= $6 >>> 0) { - $2 = $4; - break label$1; - } - $5 = HEAP32[$0 + 44 >> 2]; - HEAP32[$0 + 4 >> 2] = $5; - HEAP32[$0 + 8 >> 2] = $5 + ($4 - $6 | 0); - if (!HEAP32[$0 + 48 >> 2]) { - break label$1 - } - HEAP32[$0 + 4 >> 2] = $5 + 1; - HEAP8[($1 + $2 | 0) + -1 | 0] = HEAPU8[$5 | 0]; - } - global$0 = $3 + 32 | 0; - return $2 | 0; - } - - function __stdio_seek($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - var $4 = 0; - $4 = global$0 - 16 | 0; - global$0 = $4; - label$1 : { - if (!__wasi_syscall_ret(legalimport$__wasi_fd_seek(HEAP32[$0 + 60 >> 2], $1 | 0, $2 | 0, $3 & 255, $4 + 8 | 0) | 0)) { - $1 = HEAP32[$4 + 12 >> 2]; - $0 = HEAP32[$4 + 8 >> 2]; - break label$1; - } - HEAP32[$4 + 8 >> 2] = -1; - HEAP32[$4 + 12 >> 2] = -1; - $1 = -1; - $0 = -1; - } - global$0 = $4 + 16 | 0; - i64toi32_i32$HIGH_BITS = $1; - return $0 | 0; - } - - function fflush($0) { - var $1 = 0; - if ($0) { - if (HEAP32[$0 + 76 >> 2] <= -1) { - return __fflush_unlocked($0) - } - return __fflush_unlocked($0); - } - if (HEAP32[2794]) { - $1 = fflush(HEAP32[2794]) - } - $0 = HEAP32[3023]; - if ($0) { - while (1) { - if (HEAPU32[$0 + 20 >> 2] > HEAPU32[$0 + 28 >> 2]) { - $1 = __fflush_unlocked($0) | $1 - } - $0 = HEAP32[$0 + 56 >> 2]; - if ($0) { - continue - } - break; - } - } - return $1; - } - - function __fflush_unlocked($0) { - var $1 = 0, $2 = 0; - label$1 : { - if (HEAPU32[$0 + 20 >> 2] <= HEAPU32[$0 + 28 >> 2]) { - break label$1 - } - FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($0, 0, 0) | 0; - if (HEAP32[$0 + 20 >> 2]) { - break label$1 - } - return -1; - } - $1 = HEAP32[$0 + 4 >> 2]; - $2 = HEAP32[$0 + 8 >> 2]; - if ($1 >>> 0 < $2 >>> 0) { - $1 = $1 - $2 | 0; - FUNCTION_TABLE[HEAP32[$0 + 40 >> 2]]($0, $1, $1 >> 31, 1) | 0; - } - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - return 0; - } - - function fclose($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0; - $4 = HEAP32[$0 + 76 >> 2] >= 0 ? 1 : 0; - $3 = HEAP32[$0 >> 2] & 1; - if (!$3) { - $1 = HEAP32[$0 + 52 >> 2]; - if ($1) { - HEAP32[$1 + 56 >> 2] = HEAP32[$0 + 56 >> 2] - } - $2 = HEAP32[$0 + 56 >> 2]; - if ($2) { - HEAP32[$2 + 52 >> 2] = $1 - } - if (HEAP32[3023] == ($0 | 0)) { - HEAP32[3023] = $2 - } - } - fflush($0); - FUNCTION_TABLE[HEAP32[$0 + 12 >> 2]]($0) | 0; - $1 = HEAP32[$0 + 96 >> 2]; - if ($1) { - dlfree($1) - } - label$7 : { - if (!$3) { - dlfree($0); - break label$7; - } - if (!$4) { - break label$7 - } - } - } - - function memcmp($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0; - label$1 : { - if (!$2) { - break label$1 - } - while (1) { - $3 = HEAPU8[$0 | 0]; - $4 = HEAPU8[$1 | 0]; - if (($3 | 0) == ($4 | 0)) { - $1 = $1 + 1 | 0; - $0 = $0 + 1 | 0; - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break label$1; - } - break; - }; - $5 = $3 - $4 | 0; - } - return $5; - } - - function FLAC__cpu_info($0) { - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 3; - HEAP32[$0 + 56 >> 2] = 0; - HEAP32[$0 + 60 >> 2] = 0; - HEAP32[$0 + 48 >> 2] = 0; - HEAP32[$0 + 52 >> 2] = 0; - HEAP32[$0 + 40 >> 2] = 0; - HEAP32[$0 + 44 >> 2] = 0; - HEAP32[$0 + 32 >> 2] = 0; - HEAP32[$0 + 36 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - } - - function lround($0) { - $0 = +round(+$0); - if (Math_abs($0) < 2147483648.0) { - return ~~$0 - } - return -2147483648; - } - - function log($0) { - var $1 = 0, $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0, $6 = 0, $7 = 0.0, $8 = 0.0, $9 = 0.0, $10 = 0.0; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - wasm2js_scratch_store_f64(+$0); - $1 = wasm2js_scratch_load_i32(1) | 0; - $3 = wasm2js_scratch_load_i32(0) | 0; - if (($1 | 0) > 0 ? 1 : ($1 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { - $5 = $1; - if ($1 >>> 0 > 1048575) { - break label$4 - } - } - if (!($1 & 2147483647 | $3)) { - return -1.0 / ($0 * $0) - } - if (($1 | 0) > -1 ? 1 : 0) { - break label$3 - } - return ($0 - $0) / 0.0; - } - if ($5 >>> 0 > 2146435071) { - break label$1 - } - $1 = 1072693248; - $6 = -1023; - if (($5 | 0) != 1072693248) { - $1 = $5; - break label$2; - } - if ($3) { - break label$2 - } - return 0.0; - } - wasm2js_scratch_store_f64(+($0 * 18014398509481984.0)); - $1 = wasm2js_scratch_load_i32(1) | 0; - $3 = wasm2js_scratch_load_i32(0) | 0; - $6 = -1077; - } - $1 = $1 + 614242 | 0; - $4 = +(($1 >>> 20 | 0) + $6 | 0); - wasm2js_scratch_store_i32(0, $3 | 0); - wasm2js_scratch_store_i32(1, ($1 & 1048575) + 1072079006 | 0); - $0 = +wasm2js_scratch_load_f64() + -1.0; - $2 = $0 / ($0 + 2.0); - $7 = $4 * .6931471803691238; - $8 = $0; - $9 = $4 * 1.9082149292705877e-10; - $10 = $2; - $4 = $0 * ($0 * .5); - $2 = $2 * $2; - $0 = $2 * $2; - $0 = $7 + ($8 + ($9 + $10 * ($4 + ($0 * ($0 * ($0 * .15313837699209373 + .22222198432149784) + .3999999999940942) + $2 * ($0 * ($0 * ($0 * .14798198605116586 + .1818357216161805) + .2857142874366239) + .6666666666666735))) - $4)); - } - return $0; - } - - function FLAC__lpc_window_data($0, $1, $2, $3) { - var $4 = 0, $5 = 0; - if ($3) { - while (1) { - $5 = $4 << 2; - HEAPF32[$5 + $2 >> 2] = HEAPF32[$1 + $5 >> 2] * Math_fround(HEAP32[$0 + $5 >> 2]); - $4 = $4 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - } - } - } - - function FLAC__lpc_compute_autocorrelation($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - var $4 = 0, $5 = 0, $6 = 0, $7 = Math_fround(0), $8 = 0, $9 = 0; - $6 = $1 - $2 | 0; - label$1 : { - if (!$2) { - while (1) { - $4 = $4 + 1 | 0; - if ($4 >>> 0 <= $6 >>> 0) { - continue - } - break; - }; - break label$1; - } - $9 = memset($3, $2 << 2); - while (1) { - $7 = HEAPF32[($4 << 2) + $0 >> 2]; - $5 = 0; - while (1) { - $8 = ($5 << 2) + $9 | 0; - HEAPF32[$8 >> 2] = HEAPF32[$8 >> 2] + Math_fround($7 * HEAPF32[($4 + $5 << 2) + $0 >> 2]); - $5 = $5 + 1 | 0; - if (($5 | 0) != ($2 | 0)) { - continue - } - break; - }; - $4 = $4 + 1 | 0; - if ($4 >>> 0 <= $6 >>> 0) { - continue - } - break; - }; - } - if ($4 >>> 0 < $1 >>> 0) { - while (1) { - $2 = $1 - $4 | 0; - if ($2) { - $7 = HEAPF32[($4 << 2) + $0 >> 2]; - $5 = 0; - while (1) { - $6 = ($5 << 2) + $3 | 0; - HEAPF32[$6 >> 2] = HEAPF32[$6 >> 2] + Math_fround($7 * HEAPF32[($4 + $5 << 2) + $0 >> 2]); - $5 = $5 + 1 | 0; - if ($5 >>> 0 < $2 >>> 0) { - continue - } - break; - }; - } - $4 = $4 + 1 | 0; - if (($4 | 0) != ($1 | 0)) { - continue - } - break; - } - } - } - - function FLAC__lpc_compute_lp_coefficients($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0.0, $7 = 0, $8 = 0, $9 = 0.0, $10 = 0.0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; - $7 = global$0 - 256 | 0; - global$0 = $7; - $13 = HEAP32[$1 >> 2]; - $10 = +HEAPF32[$0 >> 2]; - label$1 : { - while (1) { - if (($5 | 0) == ($13 | 0)) { - break label$1 - } - $11 = $5 + 1 | 0; - $6 = +Math_fround(-HEAPF32[($11 << 2) + $0 >> 2]); - label$3 : { - if ($5) { - $12 = $5 >>> 1 | 0; - $4 = 0; - while (1) { - $6 = $6 - HEAPF64[($4 << 3) + $7 >> 3] * +HEAPF32[($5 - $4 << 2) + $0 >> 2]; - $4 = $4 + 1 | 0; - if (($5 | 0) != ($4 | 0)) { - continue - } - break; - }; - $6 = $6 / $10; - HEAPF64[($5 << 3) + $7 >> 3] = $6; - $4 = 0; - if ($12) { - while (1) { - $8 = ($4 << 3) + $7 | 0; - $9 = HEAPF64[$8 >> 3]; - $14 = $8; - $8 = (($4 ^ -1) + $5 << 3) + $7 | 0; - HEAPF64[$14 >> 3] = $9 + $6 * HEAPF64[$8 >> 3]; - HEAPF64[$8 >> 3] = $6 * $9 + HEAPF64[$8 >> 3]; - $4 = $4 + 1 | 0; - if (($12 | 0) != ($4 | 0)) { - continue - } - break; - } - } - if (!($5 & 1)) { - break label$3 - } - $8 = ($12 << 3) + $7 | 0; - $9 = HEAPF64[$8 >> 3]; - HEAPF64[$8 >> 3] = $9 + $6 * $9; - break label$3; - } - $6 = $6 / $10; - HEAPF64[($5 << 3) + $7 >> 3] = $6; - } - $9 = 1.0 - $6 * $6; - $4 = 0; - while (1) { - HEAPF32[(($5 << 7) + $2 | 0) + ($4 << 2) >> 2] = -Math_fround(HEAPF64[($4 << 3) + $7 >> 3]); - $4 = $4 + 1 | 0; - if ($4 >>> 0 <= $5 >>> 0) { - continue - } - break; - }; - $10 = $10 * $9; - HEAPF64[($5 << 3) + $3 >> 3] = $10; - $5 = $11; - if ($10 != 0.0) { - continue - } - break; - }; - HEAP32[$1 >> 2] = $11; - } - global$0 = $7 + 256 | 0; - } - - function FLAC__lpc_quantize_coefficients($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0.0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0.0, $12 = 0, $13 = 0, $14 = Math_fround(0); - $8 = global$0 - 16 | 0; - global$0 = $8; - label$1 : { - if (!$1) { - $7 = 2; - break label$1; - } - $5 = $2 + -1 | 0; - $2 = 0; - while (1) { - $11 = +Math_fround(Math_abs(HEAPF32[($2 << 2) + $0 >> 2])); - $6 = $6 < $11 ? $11 : $6; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - $7 = 2; - if ($6 <= 0.0) { - break label$1 - } - $9 = 1 << $5; - $12 = $9 + -1 | 0; - $10 = 0 - $9 | 0; - frexp($6, $8 + 12 | 0); - $2 = HEAP32[$8 + 12 >> 2]; - HEAP32[$8 + 12 >> 2] = $2 + -1; - $5 = $5 - $2 | 0; - HEAP32[$4 >> 2] = $5; - label$4 : { - $7 = -1 << HEAP32[1413] + -1; - $2 = $7 ^ -1; - if (($5 | 0) > ($2 | 0)) { - HEAP32[$4 >> 2] = $2; - $5 = $2; - break label$4; - } - if (($5 | 0) >= ($7 | 0)) { - break label$4 - } - $7 = 1; - break label$1; - } - $7 = 0; - if (($5 | 0) >= 0) { - if (!$1) { - break label$1 - } - $6 = 0.0; - $2 = 0; - while (1) { - $13 = $2 << 2; - $6 = $6 + +Math_fround(HEAPF32[$13 + $0 >> 2] * Math_fround(1 << $5)); - $5 = lround($6); - $5 = ($5 | 0) < ($9 | 0) ? (($5 | 0) < ($10 | 0) ? $10 : $5) : $12; - HEAP32[$3 + $13 >> 2] = $5; - $2 = $2 + 1 | 0; - if (($2 | 0) == ($1 | 0)) { - break label$1 - } - $6 = $6 - +($5 | 0); - $5 = HEAP32[$4 >> 2]; - continue; - }; - } - if ($1) { - $2 = 0; - $14 = Math_fround(1 << 0 - $5); - $6 = 0.0; - while (1) { - $7 = $2 << 2; - $6 = $6 + +Math_fround(HEAPF32[$7 + $0 >> 2] / $14); - $5 = lround($6); - $5 = ($5 | 0) < ($9 | 0) ? (($5 | 0) < ($10 | 0) ? $10 : $5) : $12; - HEAP32[$3 + $7 >> 2] = $5; - $6 = $6 - +($5 | 0); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - $7 = 0; - HEAP32[$4 >> 2] = 0; - } - global$0 = $8 + 16 | 0; - return $7; - } - - function FLAC__lpc_compute_residual_from_qlp_coefficients($0, $1, $2, $3, $4, $5) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0; - label$1 : { - if ($3 >>> 0 >= 13) { - if (($1 | 0) < 1) { - break label$1 - } - $25 = $3 + -13 | 0; - while (1) { - $17 = 0; - $20 = 0; - $19 = 0; - $22 = 0; - $21 = 0; - $24 = 0; - $23 = 0; - $26 = 0; - $18 = 0; - $16 = 0; - $15 = 0; - $14 = 0; - $13 = 0; - $12 = 0; - $11 = 0; - $10 = 0; - $9 = 0; - $8 = 0; - $7 = 0; - $3 = 0; - label$4 : { - switch ($25 | 0) { - case 19: - $17 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -128 >> 2], HEAP32[$2 + 124 >> 2]); - case 18: - $20 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -124 >> 2], HEAP32[$2 + 120 >> 2]) + $17 | 0; - case 17: - $19 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -120 >> 2], HEAP32[$2 + 116 >> 2]) + $20 | 0; - case 16: - $22 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -116 >> 2], HEAP32[$2 + 112 >> 2]) + $19 | 0; - case 15: - $21 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -112 >> 2], HEAP32[$2 + 108 >> 2]) + $22 | 0; - case 14: - $24 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -108 >> 2], HEAP32[$2 + 104 >> 2]) + $21 | 0; - case 13: - $23 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -104 >> 2], HEAP32[$2 + 100 >> 2]) + $24 | 0; - case 12: - $26 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -100 >> 2], HEAP32[$2 + 96 >> 2]) + $23 | 0; - case 11: - $18 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -96 >> 2], HEAP32[$2 + 92 >> 2]) + $26 | 0; - case 10: - $16 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -92 >> 2], HEAP32[$2 + 88 >> 2]) + $18 | 0; - case 9: - $15 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -88 >> 2], HEAP32[$2 + 84 >> 2]) + $16 | 0; - case 8: - $14 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -84 >> 2], HEAP32[$2 + 80 >> 2]) + $15 | 0; - case 7: - $13 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -80 >> 2], HEAP32[$2 + 76 >> 2]) + $14 | 0; - case 6: - $12 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -76 >> 2], HEAP32[$2 + 72 >> 2]) + $13 | 0; - case 5: - $11 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -72 >> 2], HEAP32[$2 + 68 >> 2]) + $12 | 0; - case 4: - $10 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -68 >> 2], HEAP32[$2 + 64 >> 2]) + $11 | 0; - case 3: - $9 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -64 >> 2], HEAP32[$2 + 60 >> 2]) + $10 | 0; - case 2: - $8 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -60 >> 2], HEAP32[$2 + 56 >> 2]) + $9 | 0; - case 1: - $7 = Math_imul(HEAP32[(($6 << 2) + $0 | 0) + -56 >> 2], HEAP32[$2 + 52 >> 2]) + $8 | 0; - case 0: - $3 = ($6 << 2) + $0 | 0; - $3 = ((((((((((((Math_imul(HEAP32[$3 + -52 >> 2], HEAP32[$2 + 48 >> 2]) + $7 | 0) + Math_imul(HEAP32[$3 + -48 >> 2], HEAP32[$2 + 44 >> 2]) | 0) + Math_imul(HEAP32[$3 + -44 >> 2], HEAP32[$2 + 40 >> 2]) | 0) + Math_imul(HEAP32[$3 + -40 >> 2], HEAP32[$2 + 36 >> 2]) | 0) + Math_imul(HEAP32[$3 + -36 >> 2], HEAP32[$2 + 32 >> 2]) | 0) + Math_imul(HEAP32[$3 + -32 >> 2], HEAP32[$2 + 28 >> 2]) | 0) + Math_imul(HEAP32[$3 + -28 >> 2], HEAP32[$2 + 24 >> 2]) | 0) + Math_imul(HEAP32[$3 + -24 >> 2], HEAP32[$2 + 20 >> 2]) | 0) + Math_imul(HEAP32[$3 + -20 >> 2], HEAP32[$2 + 16 >> 2]) | 0) + Math_imul(HEAP32[$3 + -16 >> 2], HEAP32[$2 + 12 >> 2]) | 0) + Math_imul(HEAP32[$3 + -12 >> 2], HEAP32[$2 + 8 >> 2]) | 0) + Math_imul(HEAP32[$3 + -8 >> 2], HEAP32[$2 + 4 >> 2]) | 0) + Math_imul(HEAP32[$3 + -4 >> 2], HEAP32[$2 >> 2]) | 0; - break; - default: - break label$4; - }; - } - $7 = $6 << 2; - HEAP32[$7 + $5 >> 2] = HEAP32[$0 + $7 >> 2] - ($3 >> $4); - $6 = $6 + 1 | 0; - if (($6 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 9) { - if ($3 >>> 0 >= 11) { - if (($3 | 0) != 12) { - if (($1 | 0) < 1) { - break label$1 - } - $15 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $10 = HEAP32[$0 + -28 >> 2]; - $11 = HEAP32[$0 + -32 >> 2]; - $12 = HEAP32[$0 + -36 >> 2]; - $13 = HEAP32[$0 + -40 >> 2]; - $16 = HEAP32[$0 + -44 >> 2]; - $18 = HEAP32[$2 >> 2]; - $17 = HEAP32[$2 + 4 >> 2]; - $20 = HEAP32[$2 + 8 >> 2]; - $19 = HEAP32[$2 + 12 >> 2]; - $22 = HEAP32[$2 + 16 >> 2]; - $21 = HEAP32[$2 + 20 >> 2]; - $24 = HEAP32[$2 + 24 >> 2]; - $23 = HEAP32[$2 + 28 >> 2]; - $26 = HEAP32[$2 + 32 >> 2]; - $25 = HEAP32[$2 + 36 >> 2]; - $28 = HEAP32[$2 + 40 >> 2]; - $2 = 0; - while (1) { - $14 = $13; - $13 = $12; - $12 = $11; - $11 = $10; - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $15; - $27 = $2 << 2; - $15 = HEAP32[$27 + $0 >> 2]; - HEAP32[$5 + $27 >> 2] = $15 - ((((((((((Math_imul($14, $25) + Math_imul($16, $28) | 0) + Math_imul($13, $26) | 0) + Math_imul($12, $23) | 0) + Math_imul($11, $24) | 0) + Math_imul($10, $21) | 0) + Math_imul($9, $22) | 0) + Math_imul($8, $19) | 0) + Math_imul($7, $20) | 0) + Math_imul($3, $17) | 0) + Math_imul($6, $18) >> $4); - $16 = $14; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $16 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $10 = HEAP32[$0 + -28 >> 2]; - $11 = HEAP32[$0 + -32 >> 2]; - $12 = HEAP32[$0 + -36 >> 2]; - $13 = HEAP32[$0 + -40 >> 2]; - $14 = HEAP32[$0 + -44 >> 2]; - $18 = HEAP32[$0 + -48 >> 2]; - $17 = HEAP32[$2 >> 2]; - $20 = HEAP32[$2 + 4 >> 2]; - $19 = HEAP32[$2 + 8 >> 2]; - $22 = HEAP32[$2 + 12 >> 2]; - $21 = HEAP32[$2 + 16 >> 2]; - $24 = HEAP32[$2 + 20 >> 2]; - $23 = HEAP32[$2 + 24 >> 2]; - $26 = HEAP32[$2 + 28 >> 2]; - $25 = HEAP32[$2 + 32 >> 2]; - $28 = HEAP32[$2 + 36 >> 2]; - $27 = HEAP32[$2 + 40 >> 2]; - $30 = HEAP32[$2 + 44 >> 2]; - $2 = 0; - while (1) { - $15 = $14; - $14 = $13; - $13 = $12; - $12 = $11; - $11 = $10; - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $16; - $29 = $2 << 2; - $16 = HEAP32[$29 + $0 >> 2]; - HEAP32[$5 + $29 >> 2] = $16 - (((((((((((Math_imul($15, $27) + Math_imul($18, $30) | 0) + Math_imul($14, $28) | 0) + Math_imul($13, $25) | 0) + Math_imul($12, $26) | 0) + Math_imul($11, $23) | 0) + Math_imul($10, $24) | 0) + Math_imul($9, $21) | 0) + Math_imul($8, $22) | 0) + Math_imul($7, $19) | 0) + Math_imul($3, $20) | 0) + Math_imul($6, $17) >> $4); - $18 = $15; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 10) { - if (($1 | 0) < 1) { - break label$1 - } - $13 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $10 = HEAP32[$0 + -28 >> 2]; - $11 = HEAP32[$0 + -32 >> 2]; - $14 = HEAP32[$0 + -36 >> 2]; - $16 = HEAP32[$2 >> 2]; - $15 = HEAP32[$2 + 4 >> 2]; - $18 = HEAP32[$2 + 8 >> 2]; - $17 = HEAP32[$2 + 12 >> 2]; - $20 = HEAP32[$2 + 16 >> 2]; - $19 = HEAP32[$2 + 20 >> 2]; - $22 = HEAP32[$2 + 24 >> 2]; - $21 = HEAP32[$2 + 28 >> 2]; - $24 = HEAP32[$2 + 32 >> 2]; - $2 = 0; - while (1) { - $12 = $11; - $11 = $10; - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $13; - $23 = $2 << 2; - $13 = HEAP32[$23 + $0 >> 2]; - HEAP32[$5 + $23 >> 2] = $13 - ((((((((Math_imul($12, $21) + Math_imul($14, $24) | 0) + Math_imul($11, $22) | 0) + Math_imul($10, $19) | 0) + Math_imul($9, $20) | 0) + Math_imul($8, $17) | 0) + Math_imul($7, $18) | 0) + Math_imul($3, $15) | 0) + Math_imul($6, $16) >> $4); - $14 = $12; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $14 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $10 = HEAP32[$0 + -28 >> 2]; - $11 = HEAP32[$0 + -32 >> 2]; - $12 = HEAP32[$0 + -36 >> 2]; - $15 = HEAP32[$0 + -40 >> 2]; - $16 = HEAP32[$2 >> 2]; - $18 = HEAP32[$2 + 4 >> 2]; - $17 = HEAP32[$2 + 8 >> 2]; - $20 = HEAP32[$2 + 12 >> 2]; - $19 = HEAP32[$2 + 16 >> 2]; - $22 = HEAP32[$2 + 20 >> 2]; - $21 = HEAP32[$2 + 24 >> 2]; - $24 = HEAP32[$2 + 28 >> 2]; - $23 = HEAP32[$2 + 32 >> 2]; - $26 = HEAP32[$2 + 36 >> 2]; - $2 = 0; - while (1) { - $13 = $12; - $12 = $11; - $11 = $10; - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $14; - $25 = $2 << 2; - $14 = HEAP32[$25 + $0 >> 2]; - HEAP32[$5 + $25 >> 2] = $14 - (((((((((Math_imul($13, $23) + Math_imul($15, $26) | 0) + Math_imul($12, $24) | 0) + Math_imul($11, $21) | 0) + Math_imul($10, $22) | 0) + Math_imul($9, $19) | 0) + Math_imul($8, $20) | 0) + Math_imul($7, $17) | 0) + Math_imul($3, $18) | 0) + Math_imul($6, $16) >> $4); - $15 = $13; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 5) { - if ($3 >>> 0 >= 7) { - if (($3 | 0) != 8) { - if (($1 | 0) < 1) { - break label$1 - } - $11 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $12 = HEAP32[$0 + -28 >> 2]; - $13 = HEAP32[$2 >> 2]; - $14 = HEAP32[$2 + 4 >> 2]; - $16 = HEAP32[$2 + 8 >> 2]; - $15 = HEAP32[$2 + 12 >> 2]; - $18 = HEAP32[$2 + 16 >> 2]; - $17 = HEAP32[$2 + 20 >> 2]; - $20 = HEAP32[$2 + 24 >> 2]; - $2 = 0; - while (1) { - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $11; - $19 = $2 << 2; - $11 = HEAP32[$19 + $0 >> 2]; - HEAP32[$5 + $19 >> 2] = $11 - ((((((Math_imul($10, $17) + Math_imul($12, $20) | 0) + Math_imul($9, $18) | 0) + Math_imul($8, $15) | 0) + Math_imul($7, $16) | 0) + Math_imul($3, $14) | 0) + Math_imul($6, $13) >> $4); - $12 = $10; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $12 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $10 = HEAP32[$0 + -28 >> 2]; - $13 = HEAP32[$0 + -32 >> 2]; - $14 = HEAP32[$2 >> 2]; - $16 = HEAP32[$2 + 4 >> 2]; - $15 = HEAP32[$2 + 8 >> 2]; - $18 = HEAP32[$2 + 12 >> 2]; - $17 = HEAP32[$2 + 16 >> 2]; - $20 = HEAP32[$2 + 20 >> 2]; - $19 = HEAP32[$2 + 24 >> 2]; - $22 = HEAP32[$2 + 28 >> 2]; - $2 = 0; - while (1) { - $11 = $10; - $10 = $9; - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $12; - $21 = $2 << 2; - $12 = HEAP32[$21 + $0 >> 2]; - HEAP32[$5 + $21 >> 2] = $12 - (((((((Math_imul($11, $19) + Math_imul($13, $22) | 0) + Math_imul($10, $20) | 0) + Math_imul($9, $17) | 0) + Math_imul($8, $18) | 0) + Math_imul($7, $15) | 0) + Math_imul($3, $16) | 0) + Math_imul($6, $14) >> $4); - $13 = $11; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 6) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $10 = HEAP32[$0 + -20 >> 2]; - $11 = HEAP32[$2 >> 2]; - $12 = HEAP32[$2 + 4 >> 2]; - $13 = HEAP32[$2 + 8 >> 2]; - $14 = HEAP32[$2 + 12 >> 2]; - $16 = HEAP32[$2 + 16 >> 2]; - $2 = 0; - while (1) { - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $9; - $15 = $2 << 2; - $9 = HEAP32[$15 + $0 >> 2]; - HEAP32[$5 + $15 >> 2] = $9 - ((((Math_imul($8, $14) + Math_imul($10, $16) | 0) + Math_imul($7, $13) | 0) + Math_imul($3, $12) | 0) + Math_imul($6, $11) >> $4); - $10 = $8; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $10 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $7 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $11 = HEAP32[$0 + -24 >> 2]; - $12 = HEAP32[$2 >> 2]; - $13 = HEAP32[$2 + 4 >> 2]; - $14 = HEAP32[$2 + 8 >> 2]; - $16 = HEAP32[$2 + 12 >> 2]; - $15 = HEAP32[$2 + 16 >> 2]; - $18 = HEAP32[$2 + 20 >> 2]; - $2 = 0; - while (1) { - $9 = $8; - $8 = $7; - $7 = $3; - $3 = $6; - $6 = $10; - $17 = $2 << 2; - $10 = HEAP32[$17 + $0 >> 2]; - HEAP32[$5 + $17 >> 2] = $10 - (((((Math_imul($9, $15) + Math_imul($11, $18) | 0) + Math_imul($8, $16) | 0) + Math_imul($7, $14) | 0) + Math_imul($3, $13) | 0) + Math_imul($6, $12) >> $4); - $11 = $9; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 3) { - if (($3 | 0) != 4) { - if (($1 | 0) < 1) { - break label$1 - } - $7 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $8 = HEAP32[$0 + -12 >> 2]; - $9 = HEAP32[$2 >> 2]; - $10 = HEAP32[$2 + 4 >> 2]; - $11 = HEAP32[$2 + 8 >> 2]; - $2 = 0; - while (1) { - $3 = $6; - $6 = $7; - $12 = $2 << 2; - $7 = HEAP32[$12 + $0 >> 2]; - HEAP32[$5 + $12 >> 2] = $7 - ((Math_imul($3, $10) + Math_imul($8, $11) | 0) + Math_imul($6, $9) >> $4); - $8 = $3; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $8 = HEAP32[$0 + -4 >> 2]; - $6 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $9 = HEAP32[$0 + -16 >> 2]; - $10 = HEAP32[$2 >> 2]; - $11 = HEAP32[$2 + 4 >> 2]; - $12 = HEAP32[$2 + 8 >> 2]; - $13 = HEAP32[$2 + 12 >> 2]; - $2 = 0; - while (1) { - $7 = $3; - $3 = $6; - $6 = $8; - $14 = $2 << 2; - $8 = HEAP32[$14 + $0 >> 2]; - HEAP32[$5 + $14 >> 2] = $8 - (((Math_imul($7, $12) + Math_imul($9, $13) | 0) + Math_imul($3, $11) | 0) + Math_imul($6, $10) >> $4); - $9 = $7; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 2) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$0 + -4 >> 2]; - $3 = HEAP32[$2 >> 2]; - $2 = 0; - while (1) { - $7 = Math_imul($3, $6); - $8 = $2 << 2; - $6 = HEAP32[$8 + $0 >> 2]; - HEAP32[$5 + $8 >> 2] = $6 - ($7 >> $4); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $3 = HEAP32[$0 + -4 >> 2]; - $7 = HEAP32[$0 + -8 >> 2]; - $8 = HEAP32[$2 >> 2]; - $9 = HEAP32[$2 + 4 >> 2]; - $2 = 0; - while (1) { - $6 = $3; - $10 = $2 << 2; - $3 = HEAP32[$10 + $0 >> 2]; - HEAP32[$5 + $10 >> 2] = $3 - (Math_imul($6, $8) + Math_imul($7, $9) >> $4); - $7 = $6; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__lpc_compute_residual_from_qlp_coefficients_wide($0, $1, $2, $3, $4, $5) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0; - label$1 : { - if ($3 >>> 0 >= 13) { - if (($1 | 0) < 1) { - break label$1 - } - $18 = $4; - $12 = $3 + -13 | 0; - while (1) { - $4 = 0; - $3 = 0; - label$4 : { - switch ($12 | 0) { - case 19: - $3 = HEAP32[(($15 << 2) + $0 | 0) + -128 >> 2]; - $4 = $3; - $7 = $3 >> 31; - $3 = HEAP32[$2 + 124 >> 2]; - $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); - $3 = i64toi32_i32$HIGH_BITS; - case 18: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -124 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 120 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 17: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -120 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 116 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 16: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -116 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 112 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 15: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -112 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 108 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 14: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -108 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 104 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 13: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -104 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 100 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 12: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -100 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 96 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 11: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -96 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 92 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 10: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -92 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 88 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 9: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -88 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 84 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 8: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -84 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 80 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 7: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -80 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 76 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 6: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -76 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 72 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 5: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -72 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 68 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 4: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -68 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 64 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 3: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -64 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 60 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 2: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -60 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 56 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 1: - $7 = HEAP32[(($15 << 2) + $0 | 0) + -56 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 52 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 0: - $8 = ($15 << 2) + $0 | 0; - $7 = HEAP32[$8 + -52 >> 2]; - $6 = $7; - $9 = $7 >> 31; - $7 = HEAP32[$2 + 48 >> 2]; - $7 = __wasm_i64_mul($6, $9, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -48 >> 2]; - $4 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 44 >> 2]; - $3 = __wasm_i64_mul($4, $9, $3, $3 >> 31); - $4 = $3 + $7 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -44 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 40 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -40 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 36 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -36 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 32 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -32 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 28 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -28 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 24 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -24 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 20 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -20 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 16 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -16 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 12 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -12 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 8 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -8 >> 2]; - $7 = $3; - $9 = $3 >> 31; - $3 = HEAP32[$2 + 4 >> 2]; - $3 = __wasm_i64_mul($7, $9, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$8 + -4 >> 2]; - $7 = $3; - $8 = $3 >> 31; - $3 = HEAP32[$2 >> 2]; - $3 = __wasm_i64_mul($7, $8, $3, $3 >> 31); - $4 = $3 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $4 >>> 0 < $3 >>> 0 ? $6 + 1 | 0 : $6; - $3 = $6; - break; - default: - break label$4; - }; - } - $7 = $15 << 2; - $6 = $7 + $5 | 0; - $9 = HEAP32[$0 + $7 >> 2]; - $7 = $3; - $3 = $18; - $8 = $3 & 31; - HEAP32[$6 >> 2] = $9 - (32 <= ($3 & 63) >>> 0 ? $7 >> $8 : ((1 << $8) - 1 & $7) << 32 - $8 | $4 >>> $8); - $15 = $15 + 1 | 0; - if (($15 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 9) { - if ($3 >>> 0 >= 11) { - if (($3 | 0) != 12) { - if (($1 | 0) < 1) { - break label$1 - } - $10 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $8 = HEAP32[$0 + -28 >> 2]; - $9 = HEAP32[$0 + -32 >> 2]; - $11 = HEAP32[$0 + -36 >> 2]; - $17 = HEAP32[$0 + -40 >> 2]; - $13 = HEAP32[$0 + -44 >> 2]; - $6 = HEAP32[$2 >> 2]; - $40 = $6; - $41 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $42 = $6; - $37 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $38 = $6; - $39 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $34 = $6; - $35 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $36 = $6; - $31 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $32 = $6; - $33 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $29 = $6; - $30 = $6 >> 31; - $6 = HEAP32[$2 + 28 >> 2]; - $26 = $6; - $27 = $6 >> 31; - $6 = HEAP32[$2 + 32 >> 2]; - $28 = $6; - $23 = $6 >> 31; - $6 = HEAP32[$2 + 36 >> 2]; - $24 = $6; - $25 = $6 >> 31; - $2 = HEAP32[$2 + 40 >> 2]; - $21 = $2; - $22 = $2 >> 31; - $2 = 0; - while (1) { - $16 = $17; - $17 = $11; - $11 = $9; - $9 = $8; - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $10; - $6 = $2 << 2; - $20 = $6 + $5 | 0; - $10 = HEAP32[$0 + $6 >> 2]; - $14 = __wasm_i64_mul($16, $16 >> 31, $24, $25); - $6 = i64toi32_i32$HIGH_BITS; - $13 = __wasm_i64_mul($13, $13 >> 31, $21, $22); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($17, $17 >> 31, $28, $23); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($11, $11 >> 31, $26, $27); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($9, $9 >> 31, $29, $30); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($8, $8 >> 31, $32, $33); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($12, $12 >> 31, $36, $31); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($7, $7 >> 31, $34, $35); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($18, $18 >> 31, $38, $39); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($3, $3 >> 31, $42, $37); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = __wasm_i64_mul($15, $15 >> 31, $40, $41); - $14 = $13 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $13 = $6; - $6 = $4; - $19 = $6 & 31; - HEAP32[$20 >> 2] = $10 - (32 <= ($6 & 63) >>> 0 ? $13 >> $19 : ((1 << $19) - 1 & $13) << 32 - $19 | $14 >>> $19); - $13 = $16; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $13 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $8 = HEAP32[$0 + -28 >> 2]; - $9 = HEAP32[$0 + -32 >> 2]; - $11 = HEAP32[$0 + -36 >> 2]; - $17 = HEAP32[$0 + -40 >> 2]; - $16 = HEAP32[$0 + -44 >> 2]; - $6 = HEAP32[$0 + -48 >> 2]; - $10 = HEAP32[$2 >> 2]; - $43 = $10; - $44 = $10 >> 31; - $10 = HEAP32[$2 + 4 >> 2]; - $45 = $10; - $40 = $10 >> 31; - $10 = HEAP32[$2 + 8 >> 2]; - $41 = $10; - $42 = $10 >> 31; - $10 = HEAP32[$2 + 12 >> 2]; - $37 = $10; - $38 = $10 >> 31; - $10 = HEAP32[$2 + 16 >> 2]; - $39 = $10; - $34 = $10 >> 31; - $10 = HEAP32[$2 + 20 >> 2]; - $35 = $10; - $36 = $10 >> 31; - $10 = HEAP32[$2 + 24 >> 2]; - $31 = $10; - $32 = $10 >> 31; - $10 = HEAP32[$2 + 28 >> 2]; - $33 = $10; - $29 = $10 >> 31; - $10 = HEAP32[$2 + 32 >> 2]; - $30 = $10; - $26 = $10 >> 31; - $10 = HEAP32[$2 + 36 >> 2]; - $27 = $10; - $28 = $10 >> 31; - $10 = HEAP32[$2 + 40 >> 2]; - $23 = $10; - $24 = $10 >> 31; - $2 = HEAP32[$2 + 44 >> 2]; - $25 = $2; - $21 = $2 >> 31; - $2 = 0; - while (1) { - $10 = $16; - $16 = $17; - $17 = $11; - $11 = $9; - $9 = $8; - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $13; - $13 = $2 << 2; - $22 = $13 + $5 | 0; - $13 = HEAP32[$0 + $13 >> 2]; - $14 = __wasm_i64_mul($10, $10 >> 31, $23, $24); - $19 = i64toi32_i32$HIGH_BITS; - $20 = $14; - $14 = __wasm_i64_mul($6, $6 >> 31, $25, $21); - $20 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $19 | 0; - $6 = $20 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($16, $16 >> 31, $27, $28); - $19 = $14 + $20 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($17, $17 >> 31, $30, $26); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($11, $11 >> 31, $33, $29); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($9, $9 >> 31, $31, $32); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($8, $8 >> 31, $35, $36); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($12, $12 >> 31, $39, $34); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($7, $7 >> 31, $37, $38); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($18, $18 >> 31, $41, $42); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($3, $3 >> 31, $45, $40); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = __wasm_i64_mul($15, $15 >> 31, $43, $44); - $19 = $14 + $19 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $19 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = $6; - $6 = $4; - $20 = $6 & 31; - HEAP32[$22 >> 2] = $13 - (32 <= ($6 & 63) >>> 0 ? $14 >> $20 : ((1 << $20) - 1 & $14) << 32 - $20 | $19 >>> $20); - $6 = $10; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 10) { - if (($1 | 0) < 1) { - break label$1 - } - $17 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $8 = HEAP32[$0 + -28 >> 2]; - $9 = HEAP32[$0 + -32 >> 2]; - $16 = HEAP32[$0 + -36 >> 2]; - $11 = HEAP32[$2 >> 2]; - $34 = $11; - $35 = $11 >> 31; - $11 = HEAP32[$2 + 4 >> 2]; - $36 = $11; - $31 = $11 >> 31; - $11 = HEAP32[$2 + 8 >> 2]; - $32 = $11; - $33 = $11 >> 31; - $11 = HEAP32[$2 + 12 >> 2]; - $29 = $11; - $30 = $11 >> 31; - $11 = HEAP32[$2 + 16 >> 2]; - $26 = $11; - $27 = $11 >> 31; - $11 = HEAP32[$2 + 20 >> 2]; - $28 = $11; - $23 = $11 >> 31; - $11 = HEAP32[$2 + 24 >> 2]; - $24 = $11; - $25 = $11 >> 31; - $11 = HEAP32[$2 + 28 >> 2]; - $21 = $11; - $22 = $11 >> 31; - $2 = HEAP32[$2 + 32 >> 2]; - $20 = $2; - $19 = $2 >> 31; - $2 = 0; - while (1) { - $11 = $9; - $9 = $8; - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $17; - $6 = $2 << 2; - $14 = $6 + $5 | 0; - $17 = HEAP32[$0 + $6 >> 2]; - $10 = __wasm_i64_mul($11, $11 >> 31, $21, $22); - $6 = i64toi32_i32$HIGH_BITS; - $16 = __wasm_i64_mul($16, $16 >> 31, $20, $19); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($9, $9 >> 31, $24, $25); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($8, $8 >> 31, $28, $23); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($12, $12 >> 31, $26, $27); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($7, $7 >> 31, $29, $30); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($18, $18 >> 31, $32, $33); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($3, $3 >> 31, $36, $31); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = __wasm_i64_mul($15, $15 >> 31, $34, $35); - $10 = $16 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $6; - $6 = $4; - $13 = $6 & 31; - HEAP32[$14 >> 2] = $17 - (32 <= ($6 & 63) >>> 0 ? $16 >> $13 : ((1 << $13) - 1 & $16) << 32 - $13 | $10 >>> $13); - $16 = $11; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $16 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $8 = HEAP32[$0 + -28 >> 2]; - $9 = HEAP32[$0 + -32 >> 2]; - $11 = HEAP32[$0 + -36 >> 2]; - $10 = HEAP32[$0 + -40 >> 2]; - $6 = HEAP32[$2 >> 2]; - $37 = $6; - $38 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $39 = $6; - $34 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $35 = $6; - $36 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $31 = $6; - $32 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $33 = $6; - $29 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $30 = $6; - $26 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $27 = $6; - $28 = $6 >> 31; - $6 = HEAP32[$2 + 28 >> 2]; - $23 = $6; - $24 = $6 >> 31; - $6 = HEAP32[$2 + 32 >> 2]; - $25 = $6; - $21 = $6 >> 31; - $2 = HEAP32[$2 + 36 >> 2]; - $22 = $2; - $20 = $2 >> 31; - $2 = 0; - while (1) { - $17 = $11; - $11 = $9; - $9 = $8; - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $16; - $6 = $2 << 2; - $19 = $6 + $5 | 0; - $16 = HEAP32[$0 + $6 >> 2]; - $13 = __wasm_i64_mul($17, $17 >> 31, $25, $21); - $6 = i64toi32_i32$HIGH_BITS; - $10 = __wasm_i64_mul($10, $10 >> 31, $22, $20); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($11, $11 >> 31, $23, $24); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($9, $9 >> 31, $27, $28); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($8, $8 >> 31, $30, $26); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($12, $12 >> 31, $33, $29); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($7, $7 >> 31, $31, $32); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($18, $18 >> 31, $35, $36); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($3, $3 >> 31, $39, $34); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = __wasm_i64_mul($15, $15 >> 31, $37, $38); - $13 = $10 + $13 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = $6; - $6 = $4; - $14 = $6 & 31; - HEAP32[$19 >> 2] = $16 - (32 <= ($6 & 63) >>> 0 ? $10 >> $14 : ((1 << $14) - 1 & $10) << 32 - $14 | $13 >>> $14); - $10 = $17; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 5) { - if ($3 >>> 0 >= 7) { - if (($3 | 0) != 8) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $11 = HEAP32[$0 + -28 >> 2]; - $8 = HEAP32[$2 >> 2]; - $29 = $8; - $30 = $8 >> 31; - $8 = HEAP32[$2 + 4 >> 2]; - $26 = $8; - $27 = $8 >> 31; - $8 = HEAP32[$2 + 8 >> 2]; - $28 = $8; - $23 = $8 >> 31; - $8 = HEAP32[$2 + 12 >> 2]; - $24 = $8; - $25 = $8 >> 31; - $8 = HEAP32[$2 + 16 >> 2]; - $21 = $8; - $22 = $8 >> 31; - $8 = HEAP32[$2 + 20 >> 2]; - $20 = $8; - $19 = $8 >> 31; - $2 = HEAP32[$2 + 24 >> 2]; - $14 = $2; - $13 = $2 >> 31; - $2 = 0; - while (1) { - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $9; - $9 = $2 << 2; - $10 = $9 + $5 | 0; - $9 = HEAP32[$0 + $9 >> 2]; - $17 = __wasm_i64_mul($8, $8 >> 31, $20, $19); - $6 = i64toi32_i32$HIGH_BITS; - $11 = __wasm_i64_mul($11, $11 >> 31, $14, $13); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = __wasm_i64_mul($12, $12 >> 31, $21, $22); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = __wasm_i64_mul($7, $7 >> 31, $24, $25); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = __wasm_i64_mul($18, $18 >> 31, $28, $23); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = __wasm_i64_mul($3, $3 >> 31, $26, $27); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = __wasm_i64_mul($15, $15 >> 31, $29, $30); - $17 = $11 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $17 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $4 & 31; - HEAP32[$10 >> 2] = $9 - (32 <= ($4 & 63) >>> 0 ? $6 >> $16 : ((1 << $16) - 1 & $6) << 32 - $16 | $17 >>> $16); - $11 = $8; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $11 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $12 = HEAP32[$0 + -24 >> 2]; - $8 = HEAP32[$0 + -28 >> 2]; - $17 = HEAP32[$0 + -32 >> 2]; - $9 = HEAP32[$2 >> 2]; - $31 = $9; - $32 = $9 >> 31; - $9 = HEAP32[$2 + 4 >> 2]; - $33 = $9; - $29 = $9 >> 31; - $9 = HEAP32[$2 + 8 >> 2]; - $30 = $9; - $26 = $9 >> 31; - $9 = HEAP32[$2 + 12 >> 2]; - $27 = $9; - $28 = $9 >> 31; - $9 = HEAP32[$2 + 16 >> 2]; - $23 = $9; - $24 = $9 >> 31; - $9 = HEAP32[$2 + 20 >> 2]; - $25 = $9; - $21 = $9 >> 31; - $9 = HEAP32[$2 + 24 >> 2]; - $22 = $9; - $20 = $9 >> 31; - $2 = HEAP32[$2 + 28 >> 2]; - $19 = $2; - $14 = $2 >> 31; - $2 = 0; - while (1) { - $9 = $8; - $8 = $12; - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $11; - $11 = $2 << 2; - $13 = $11 + $5 | 0; - $11 = HEAP32[$0 + $11 >> 2]; - $16 = __wasm_i64_mul($9, $9 >> 31, $22, $20); - $6 = i64toi32_i32$HIGH_BITS; - $17 = __wasm_i64_mul($17, $17 >> 31, $19, $14); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($8, $8 >> 31, $25, $21); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($12, $12 >> 31, $23, $24); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($7, $7 >> 31, $27, $28); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($18, $18 >> 31, $30, $26); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($3, $3 >> 31, $33, $29); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = __wasm_i64_mul($15, $15 >> 31, $31, $32); - $16 = $17 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $6; - $6 = $4; - $10 = $6 & 31; - HEAP32[$13 >> 2] = $11 - (32 <= ($6 & 63) >>> 0 ? $17 >> $10 : ((1 << $10) - 1 & $17) << 32 - $10 | $16 >>> $10); - $17 = $9; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 6) { - if (($1 | 0) < 1) { - break label$1 - } - $12 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $8 = HEAP32[$0 + -20 >> 2]; - $7 = HEAP32[$2 >> 2]; - $23 = $7; - $24 = $7 >> 31; - $7 = HEAP32[$2 + 4 >> 2]; - $25 = $7; - $21 = $7 >> 31; - $7 = HEAP32[$2 + 8 >> 2]; - $22 = $7; - $20 = $7 >> 31; - $7 = HEAP32[$2 + 12 >> 2]; - $19 = $7; - $14 = $7 >> 31; - $2 = HEAP32[$2 + 16 >> 2]; - $13 = $2; - $10 = $2 >> 31; - $2 = 0; - while (1) { - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $12; - $12 = $2 << 2; - $16 = $12 + $5 | 0; - $12 = HEAP32[$0 + $12 >> 2]; - $11 = __wasm_i64_mul($7, $7 >> 31, $19, $14); - $9 = i64toi32_i32$HIGH_BITS; - $8 = __wasm_i64_mul($8, $8 >> 31, $13, $10); - $11 = $8 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $9 | 0; - $6 = $11 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $8 = __wasm_i64_mul($18, $18 >> 31, $22, $20); - $9 = $8 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $8 = __wasm_i64_mul($3, $3 >> 31, $25, $21); - $9 = $8 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $8 = __wasm_i64_mul($15, $15 >> 31, $23, $24); - $9 = $8 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $11 = $4 & 31; - HEAP32[$16 >> 2] = $12 - (32 <= ($4 & 63) >>> 0 ? $6 >> $11 : ((1 << $11) - 1 & $6) << 32 - $11 | $9 >>> $11); - $8 = $7; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $8 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $18 = HEAP32[$0 + -16 >> 2]; - $7 = HEAP32[$0 + -20 >> 2]; - $9 = HEAP32[$0 + -24 >> 2]; - $12 = HEAP32[$2 >> 2]; - $27 = $12; - $28 = $12 >> 31; - $12 = HEAP32[$2 + 4 >> 2]; - $23 = $12; - $24 = $12 >> 31; - $12 = HEAP32[$2 + 8 >> 2]; - $25 = $12; - $21 = $12 >> 31; - $12 = HEAP32[$2 + 12 >> 2]; - $22 = $12; - $20 = $12 >> 31; - $12 = HEAP32[$2 + 16 >> 2]; - $19 = $12; - $14 = $12 >> 31; - $2 = HEAP32[$2 + 20 >> 2]; - $13 = $2; - $10 = $2 >> 31; - $2 = 0; - while (1) { - $12 = $7; - $7 = $18; - $18 = $3; - $3 = $15; - $15 = $8; - $8 = $2 << 2; - $16 = $8 + $5 | 0; - $8 = HEAP32[$0 + $8 >> 2]; - $6 = __wasm_i64_mul($12, $12 >> 31, $19, $14); - $11 = i64toi32_i32$HIGH_BITS; - $9 = __wasm_i64_mul($9, $9 >> 31, $13, $10); - $26 = $9 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $11 | 0; - $6 = $26 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = __wasm_i64_mul($7, $7 >> 31, $22, $20); - $11 = $9 + $26 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = __wasm_i64_mul($18, $18 >> 31, $25, $21); - $11 = $9 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = __wasm_i64_mul($3, $3 >> 31, $23, $24); - $11 = $9 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = __wasm_i64_mul($15, $15 >> 31, $27, $28); - $11 = $9 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $4 & 31; - HEAP32[$16 >> 2] = $8 - (32 <= ($4 & 63) >>> 0 ? $6 >> $17 : ((1 << $17) - 1 & $6) << 32 - $17 | $11 >>> $17); - $9 = $12; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 3) { - if (($3 | 0) != 4) { - if (($1 | 0) < 1) { - break label$1 - } - $18 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $7 = HEAP32[$0 + -12 >> 2]; - $3 = HEAP32[$2 >> 2]; - $19 = $3; - $14 = $3 >> 31; - $3 = HEAP32[$2 + 4 >> 2]; - $13 = $3; - $10 = $3 >> 31; - $2 = HEAP32[$2 + 8 >> 2]; - $16 = $2; - $17 = $2 >> 31; - $2 = 0; - while (1) { - $3 = $15; - $15 = $18; - $18 = $2 << 2; - $11 = $18 + $5 | 0; - $18 = HEAP32[$0 + $18 >> 2]; - $9 = $18; - $8 = __wasm_i64_mul($3, $3 >> 31, $13, $10); - $12 = i64toi32_i32$HIGH_BITS; - $7 = __wasm_i64_mul($7, $7 >> 31, $16, $17); - $8 = $7 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $12 | 0; - $6 = $8 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; - $7 = __wasm_i64_mul($15, $15 >> 31, $19, $14); - $12 = $7 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; - $7 = $4; - $8 = $7 & 31; - HEAP32[$11 >> 2] = $9 - (32 <= ($7 & 63) >>> 0 ? $6 >> $8 : ((1 << $8) - 1 & $6) << 32 - $8 | $12 >>> $8); - $7 = $3; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $7 = HEAP32[$0 + -4 >> 2]; - $15 = HEAP32[$0 + -8 >> 2]; - $3 = HEAP32[$0 + -12 >> 2]; - $12 = HEAP32[$0 + -16 >> 2]; - $18 = HEAP32[$2 >> 2]; - $21 = $18; - $22 = $18 >> 31; - $18 = HEAP32[$2 + 4 >> 2]; - $20 = $18; - $19 = $18 >> 31; - $18 = HEAP32[$2 + 8 >> 2]; - $14 = $18; - $13 = $14 >> 31; - $2 = HEAP32[$2 + 12 >> 2]; - $10 = $2; - $16 = $2 >> 31; - $2 = 0; - while (1) { - $18 = $3; - $3 = $15; - $15 = $7; - $7 = $2 << 2; - $17 = $7 + $5 | 0; - $7 = HEAP32[$0 + $7 >> 2]; - $9 = __wasm_i64_mul($18, $18 >> 31, $14, $13); - $8 = i64toi32_i32$HIGH_BITS; - $12 = __wasm_i64_mul($12, $12 >> 31, $10, $16); - $9 = $12 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $8 | 0; - $6 = $9 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $12 = __wasm_i64_mul($3, $3 >> 31, $20, $19); - $8 = $12 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $12 = __wasm_i64_mul($15, $15 >> 31, $21, $22); - $8 = $12 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $4 & 31; - HEAP32[$17 >> 2] = $7 - (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $8 >>> $9); - $12 = $18; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 2) { - if (($1 | 0) < 1) { - break label$1 - } - $15 = HEAP32[$0 + -4 >> 2]; - $2 = HEAP32[$2 >> 2]; - $9 = $2; - $8 = $2 >> 31; - $2 = 0; - while (1) { - $3 = $2 << 2; - $6 = $3 + $5 | 0; - $18 = HEAP32[$0 + $3 >> 2]; - $15 = __wasm_i64_mul($15, $15 >> 31, $9, $8); - $7 = i64toi32_i32$HIGH_BITS; - $3 = $4; - $12 = $3 & 31; - HEAP32[$6 >> 2] = $18 - (32 <= ($3 & 63) >>> 0 ? $7 >> $12 : ((1 << $12) - 1 & $7) << 32 - $12 | $15 >>> $12); - $15 = $18; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $3 = HEAP32[$0 + -4 >> 2]; - $18 = HEAP32[$0 + -8 >> 2]; - $15 = HEAP32[$2 >> 2]; - $10 = $15; - $16 = $10 >> 31; - $2 = HEAP32[$2 + 4 >> 2]; - $17 = $2; - $11 = $2 >> 31; - $2 = 0; - while (1) { - $15 = $3; - $3 = $2 << 2; - $9 = $3 + $5 | 0; - $3 = HEAP32[$0 + $3 >> 2]; - $12 = __wasm_i64_mul($15, $15 >> 31, $10, $16); - $7 = i64toi32_i32$HIGH_BITS; - $18 = __wasm_i64_mul($18, $18 >> 31, $17, $11); - $12 = $18 + $12 | 0; - $6 = i64toi32_i32$HIGH_BITS + $7 | 0; - $6 = $12 >>> 0 < $18 >>> 0 ? $6 + 1 | 0 : $6; - $7 = $12; - $12 = $4 & 31; - HEAP32[$9 >> 2] = $3 - (32 <= ($4 & 63) >>> 0 ? $6 >> $12 : ((1 << $12) - 1 & $6) << 32 - $12 | $7 >>> $12); - $18 = $15; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__lpc_restore_signal($0, $1, $2, $3, $4, $5) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0; - label$1 : { - if ($3 >>> 0 >= 13) { - if (($1 | 0) < 1) { - break label$1 - } - $17 = $3 + -13 | 0; - while (1) { - $25 = 0; - $26 = 0; - $23 = 0; - $24 = 0; - $21 = 0; - $22 = 0; - $19 = 0; - $20 = 0; - $18 = 0; - $15 = 0; - $12 = 0; - $10 = 0; - $14 = 0; - $9 = 0; - $13 = 0; - $7 = 0; - $16 = 0; - $11 = 0; - $8 = 0; - $3 = 0; - label$4 : { - switch ($17 | 0) { - case 19: - $25 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -128 >> 2], HEAP32[$2 + 124 >> 2]); - case 18: - $26 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -124 >> 2], HEAP32[$2 + 120 >> 2]) + $25 | 0; - case 17: - $23 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -120 >> 2], HEAP32[$2 + 116 >> 2]) + $26 | 0; - case 16: - $24 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -116 >> 2], HEAP32[$2 + 112 >> 2]) + $23 | 0; - case 15: - $21 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -112 >> 2], HEAP32[$2 + 108 >> 2]) + $24 | 0; - case 14: - $22 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -108 >> 2], HEAP32[$2 + 104 >> 2]) + $21 | 0; - case 13: - $19 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -104 >> 2], HEAP32[$2 + 100 >> 2]) + $22 | 0; - case 12: - $20 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -100 >> 2], HEAP32[$2 + 96 >> 2]) + $19 | 0; - case 11: - $18 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -96 >> 2], HEAP32[$2 + 92 >> 2]) + $20 | 0; - case 10: - $15 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -92 >> 2], HEAP32[$2 + 88 >> 2]) + $18 | 0; - case 9: - $12 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -88 >> 2], HEAP32[$2 + 84 >> 2]) + $15 | 0; - case 8: - $10 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -84 >> 2], HEAP32[$2 + 80 >> 2]) + $12 | 0; - case 7: - $14 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -80 >> 2], HEAP32[$2 + 76 >> 2]) + $10 | 0; - case 6: - $9 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -76 >> 2], HEAP32[$2 + 72 >> 2]) + $14 | 0; - case 5: - $13 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -72 >> 2], HEAP32[$2 + 68 >> 2]) + $9 | 0; - case 4: - $7 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -68 >> 2], HEAP32[$2 + 64 >> 2]) + $13 | 0; - case 3: - $16 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -64 >> 2], HEAP32[$2 + 60 >> 2]) + $7 | 0; - case 2: - $11 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -60 >> 2], HEAP32[$2 + 56 >> 2]) + $16 | 0; - case 1: - $8 = Math_imul(HEAP32[(($6 << 2) + $5 | 0) + -56 >> 2], HEAP32[$2 + 52 >> 2]) + $11 | 0; - case 0: - $3 = ($6 << 2) + $5 | 0; - $3 = ((((((((((((Math_imul(HEAP32[$3 + -52 >> 2], HEAP32[$2 + 48 >> 2]) + $8 | 0) + Math_imul(HEAP32[$3 + -48 >> 2], HEAP32[$2 + 44 >> 2]) | 0) + Math_imul(HEAP32[$3 + -44 >> 2], HEAP32[$2 + 40 >> 2]) | 0) + Math_imul(HEAP32[$3 + -40 >> 2], HEAP32[$2 + 36 >> 2]) | 0) + Math_imul(HEAP32[$3 + -36 >> 2], HEAP32[$2 + 32 >> 2]) | 0) + Math_imul(HEAP32[$3 + -32 >> 2], HEAP32[$2 + 28 >> 2]) | 0) + Math_imul(HEAP32[$3 + -28 >> 2], HEAP32[$2 + 24 >> 2]) | 0) + Math_imul(HEAP32[$3 + -24 >> 2], HEAP32[$2 + 20 >> 2]) | 0) + Math_imul(HEAP32[$3 + -20 >> 2], HEAP32[$2 + 16 >> 2]) | 0) + Math_imul(HEAP32[$3 + -16 >> 2], HEAP32[$2 + 12 >> 2]) | 0) + Math_imul(HEAP32[$3 + -12 >> 2], HEAP32[$2 + 8 >> 2]) | 0) + Math_imul(HEAP32[$3 + -8 >> 2], HEAP32[$2 + 4 >> 2]) | 0) + Math_imul(HEAP32[$3 + -4 >> 2], HEAP32[$2 >> 2]) | 0; - break; - default: - break label$4; - }; - } - $8 = $6 << 2; - HEAP32[$8 + $5 >> 2] = HEAP32[$0 + $8 >> 2] + ($3 >> $4); - $6 = $6 + 1 | 0; - if (($6 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 9) { - if ($3 >>> 0 >= 11) { - if (($3 | 0) != 12) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $9 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $10 = HEAP32[$5 + -40 >> 2]; - $12 = HEAP32[$5 + -44 >> 2]; - $27 = HEAP32[$2 >> 2]; - $28 = HEAP32[$2 + 4 >> 2]; - $25 = HEAP32[$2 + 8 >> 2]; - $26 = HEAP32[$2 + 12 >> 2]; - $23 = HEAP32[$2 + 16 >> 2]; - $24 = HEAP32[$2 + 20 >> 2]; - $21 = HEAP32[$2 + 24 >> 2]; - $22 = HEAP32[$2 + 28 >> 2]; - $19 = HEAP32[$2 + 32 >> 2]; - $20 = HEAP32[$2 + 36 >> 2]; - $18 = HEAP32[$2 + 40 >> 2]; - $2 = 0; - while (1) { - $17 = $10; - $12 = Math_imul($10, $20) + Math_imul($12, $18) | 0; - $10 = $14; - $12 = $12 + Math_imul($19, $10) | 0; - $14 = $9; - $12 = Math_imul($9, $22) + $12 | 0; - $9 = $13; - $12 = $12 + Math_imul($21, $9) | 0; - $13 = $7; - $12 = Math_imul($7, $24) + $12 | 0; - $7 = $16; - $12 = $12 + Math_imul($23, $7) | 0; - $16 = $11; - $12 = Math_imul($11, $26) + $12 | 0; - $11 = $8; - $15 = Math_imul($8, $25) + $12 | 0; - $8 = $3; - $12 = $2 << 2; - $15 = Math_imul($3, $28) + $15 | 0; - $3 = $6; - $6 = HEAP32[$12 + $0 >> 2] + ($15 + Math_imul($27, $3) >> $4) | 0; - HEAP32[$5 + $12 >> 2] = $6; - $12 = $17; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $9 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $10 = HEAP32[$5 + -40 >> 2]; - $12 = HEAP32[$5 + -44 >> 2]; - $15 = HEAP32[$5 + -48 >> 2]; - $29 = HEAP32[$2 >> 2]; - $30 = HEAP32[$2 + 4 >> 2]; - $27 = HEAP32[$2 + 8 >> 2]; - $28 = HEAP32[$2 + 12 >> 2]; - $25 = HEAP32[$2 + 16 >> 2]; - $26 = HEAP32[$2 + 20 >> 2]; - $23 = HEAP32[$2 + 24 >> 2]; - $24 = HEAP32[$2 + 28 >> 2]; - $21 = HEAP32[$2 + 32 >> 2]; - $22 = HEAP32[$2 + 36 >> 2]; - $19 = HEAP32[$2 + 40 >> 2]; - $20 = HEAP32[$2 + 44 >> 2]; - $2 = 0; - while (1) { - $17 = $12; - $15 = Math_imul($12, $19) + Math_imul($15, $20) | 0; - $12 = $10; - $15 = Math_imul($10, $22) + $15 | 0; - $10 = $14; - $15 = $15 + Math_imul($21, $10) | 0; - $14 = $9; - $15 = Math_imul($9, $24) + $15 | 0; - $9 = $13; - $15 = $15 + Math_imul($23, $9) | 0; - $13 = $7; - $15 = Math_imul($7, $26) + $15 | 0; - $7 = $16; - $15 = $15 + Math_imul($25, $7) | 0; - $16 = $11; - $15 = Math_imul($11, $28) + $15 | 0; - $11 = $8; - $18 = Math_imul($8, $27) + $15 | 0; - $8 = $3; - $15 = $2 << 2; - $18 = Math_imul($3, $30) + $18 | 0; - $3 = $6; - $6 = HEAP32[$15 + $0 >> 2] + ($18 + Math_imul($29, $3) >> $4) | 0; - HEAP32[$5 + $15 >> 2] = $6; - $15 = $17; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 10) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $9 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $23 = HEAP32[$2 >> 2]; - $24 = HEAP32[$2 + 4 >> 2]; - $21 = HEAP32[$2 + 8 >> 2]; - $22 = HEAP32[$2 + 12 >> 2]; - $19 = HEAP32[$2 + 16 >> 2]; - $20 = HEAP32[$2 + 20 >> 2]; - $18 = HEAP32[$2 + 24 >> 2]; - $15 = HEAP32[$2 + 28 >> 2]; - $17 = HEAP32[$2 + 32 >> 2]; - $2 = 0; - while (1) { - $10 = $9; - $14 = Math_imul($9, $15) + Math_imul($14, $17) | 0; - $9 = $13; - $14 = $14 + Math_imul($18, $9) | 0; - $13 = $7; - $14 = Math_imul($7, $20) + $14 | 0; - $7 = $16; - $14 = $14 + Math_imul($19, $7) | 0; - $16 = $11; - $14 = Math_imul($11, $22) + $14 | 0; - $11 = $8; - $12 = Math_imul($8, $21) + $14 | 0; - $8 = $3; - $14 = $2 << 2; - $12 = Math_imul($3, $24) + $12 | 0; - $3 = $6; - $6 = HEAP32[$14 + $0 >> 2] + ($12 + Math_imul($23, $3) >> $4) | 0; - HEAP32[$5 + $14 >> 2] = $6; - $14 = $10; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $9 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $10 = HEAP32[$5 + -40 >> 2]; - $25 = HEAP32[$2 >> 2]; - $26 = HEAP32[$2 + 4 >> 2]; - $23 = HEAP32[$2 + 8 >> 2]; - $24 = HEAP32[$2 + 12 >> 2]; - $21 = HEAP32[$2 + 16 >> 2]; - $22 = HEAP32[$2 + 20 >> 2]; - $19 = HEAP32[$2 + 24 >> 2]; - $20 = HEAP32[$2 + 28 >> 2]; - $18 = HEAP32[$2 + 32 >> 2]; - $15 = HEAP32[$2 + 36 >> 2]; - $2 = 0; - while (1) { - $12 = $14; - $10 = Math_imul($18, $12) + Math_imul($10, $15) | 0; - $14 = $9; - $10 = Math_imul($9, $20) + $10 | 0; - $9 = $13; - $10 = $10 + Math_imul($19, $9) | 0; - $13 = $7; - $10 = Math_imul($7, $22) + $10 | 0; - $7 = $16; - $10 = $10 + Math_imul($21, $7) | 0; - $16 = $11; - $10 = Math_imul($11, $24) + $10 | 0; - $11 = $8; - $17 = Math_imul($8, $23) + $10 | 0; - $8 = $3; - $10 = $2 << 2; - $17 = Math_imul($3, $26) + $17 | 0; - $3 = $6; - $6 = HEAP32[$10 + $0 >> 2] + ($17 + Math_imul($25, $3) >> $4) | 0; - HEAP32[$5 + $10 >> 2] = $6; - $10 = $12; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 5) { - if ($3 >>> 0 >= 7) { - if (($3 | 0) != 8) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $19 = HEAP32[$2 >> 2]; - $20 = HEAP32[$2 + 4 >> 2]; - $18 = HEAP32[$2 + 8 >> 2]; - $15 = HEAP32[$2 + 12 >> 2]; - $17 = HEAP32[$2 + 16 >> 2]; - $12 = HEAP32[$2 + 20 >> 2]; - $10 = HEAP32[$2 + 24 >> 2]; - $2 = 0; - while (1) { - $9 = $7; - $13 = Math_imul($7, $12) + Math_imul($10, $13) | 0; - $7 = $16; - $13 = $13 + Math_imul($17, $7) | 0; - $16 = $11; - $13 = Math_imul($11, $15) + $13 | 0; - $11 = $8; - $14 = Math_imul($8, $18) + $13 | 0; - $8 = $3; - $13 = $2 << 2; - $14 = Math_imul($3, $20) + $14 | 0; - $3 = $6; - $6 = HEAP32[$13 + $0 >> 2] + ($14 + Math_imul($19, $3) >> $4) | 0; - HEAP32[$5 + $13 >> 2] = $6; - $13 = $9; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $13 = HEAP32[$5 + -28 >> 2]; - $9 = HEAP32[$5 + -32 >> 2]; - $21 = HEAP32[$2 >> 2]; - $22 = HEAP32[$2 + 4 >> 2]; - $19 = HEAP32[$2 + 8 >> 2]; - $20 = HEAP32[$2 + 12 >> 2]; - $18 = HEAP32[$2 + 16 >> 2]; - $15 = HEAP32[$2 + 20 >> 2]; - $17 = HEAP32[$2 + 24 >> 2]; - $12 = HEAP32[$2 + 28 >> 2]; - $2 = 0; - while (1) { - $14 = $13; - $9 = Math_imul($17, $13) + Math_imul($9, $12) | 0; - $13 = $7; - $9 = Math_imul($7, $15) + $9 | 0; - $7 = $16; - $9 = $9 + Math_imul($18, $7) | 0; - $16 = $11; - $9 = Math_imul($11, $20) + $9 | 0; - $11 = $8; - $10 = Math_imul($8, $19) + $9 | 0; - $8 = $3; - $9 = $2 << 2; - $10 = Math_imul($3, $22) + $10 | 0; - $3 = $6; - $6 = HEAP32[$9 + $0 >> 2] + ($10 + Math_imul($21, $3) >> $4) | 0; - HEAP32[$5 + $9 >> 2] = $6; - $9 = $14; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 6) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $17 = HEAP32[$2 >> 2]; - $12 = HEAP32[$2 + 4 >> 2]; - $10 = HEAP32[$2 + 8 >> 2]; - $14 = HEAP32[$2 + 12 >> 2]; - $9 = HEAP32[$2 + 16 >> 2]; - $2 = 0; - while (1) { - $7 = $11; - $16 = Math_imul($14, $7) + Math_imul($9, $16) | 0; - $11 = $8; - $13 = Math_imul($8, $10) + $16 | 0; - $8 = $3; - $16 = $2 << 2; - $13 = Math_imul($3, $12) + $13 | 0; - $3 = $6; - $6 = HEAP32[$16 + $0 >> 2] + ($13 + Math_imul($17, $3) >> $4) | 0; - HEAP32[$5 + $16 >> 2] = $6; - $16 = $7; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $16 = HEAP32[$5 + -20 >> 2]; - $7 = HEAP32[$5 + -24 >> 2]; - $18 = HEAP32[$2 >> 2]; - $15 = HEAP32[$2 + 4 >> 2]; - $17 = HEAP32[$2 + 8 >> 2]; - $12 = HEAP32[$2 + 12 >> 2]; - $10 = HEAP32[$2 + 16 >> 2]; - $14 = HEAP32[$2 + 20 >> 2]; - $2 = 0; - while (1) { - $13 = $16; - $7 = Math_imul($10, $13) + Math_imul($7, $14) | 0; - $16 = $11; - $7 = Math_imul($11, $12) + $7 | 0; - $11 = $8; - $9 = Math_imul($8, $17) + $7 | 0; - $8 = $3; - $7 = $2 << 2; - $9 = Math_imul($3, $15) + $9 | 0; - $3 = $6; - $6 = HEAP32[$7 + $0 >> 2] + ($9 + Math_imul($18, $3) >> $4) | 0; - HEAP32[$5 + $7 >> 2] = $6; - $7 = $13; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 3) { - if (($3 | 0) != 4) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $9 = HEAP32[$2 >> 2]; - $13 = HEAP32[$2 + 4 >> 2]; - $7 = HEAP32[$2 + 8 >> 2]; - $2 = 0; - while (1) { - $11 = $3; - $16 = $2 << 2; - $8 = Math_imul($3, $13) + Math_imul($8, $7) | 0; - $3 = $6; - $6 = HEAP32[$16 + $0 >> 2] + ($8 + Math_imul($9, $3) >> $4) | 0; - HEAP32[$5 + $16 >> 2] = $6; - $8 = $11; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $8 = HEAP32[$5 + -12 >> 2]; - $11 = HEAP32[$5 + -16 >> 2]; - $10 = HEAP32[$2 >> 2]; - $14 = HEAP32[$2 + 4 >> 2]; - $9 = HEAP32[$2 + 8 >> 2]; - $13 = HEAP32[$2 + 12 >> 2]; - $2 = 0; - while (1) { - $16 = $8; - $7 = Math_imul($8, $9) + Math_imul($11, $13) | 0; - $8 = $3; - $11 = $2 << 2; - $7 = Math_imul($3, $14) + $7 | 0; - $3 = $6; - $6 = HEAP32[$11 + $0 >> 2] + ($7 + Math_imul($10, $3) >> $4) | 0; - HEAP32[$5 + $11 >> 2] = $6; - $11 = $16; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 2) { - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $8 = HEAP32[$2 >> 2]; - $2 = 0; - while (1) { - $3 = $2 << 2; - $6 = HEAP32[$3 + $0 >> 2] + (Math_imul($6, $8) >> $4) | 0; - HEAP32[$3 + $5 >> 2] = $6; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $6 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $7 = HEAP32[$2 >> 2]; - $16 = HEAP32[$2 + 4 >> 2]; - $2 = 0; - while (1) { - $8 = $6; - $11 = $2 << 2; - $6 = HEAP32[$11 + $0 >> 2] + (Math_imul($6, $7) + Math_imul($3, $16) >> $4) | 0; - HEAP32[$5 + $11 >> 2] = $6; - $3 = $8; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__lpc_restore_signal_wide($0, $1, $2, $3, $4, $5) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0, $46 = 0; - label$1 : { - if ($3 >>> 0 >= 13) { - if (($1 | 0) < 1) { - break label$1 - } - $13 = $4; - $12 = $3 + -13 | 0; - while (1) { - $4 = 0; - $3 = 0; - label$4 : { - switch ($12 | 0) { - case 19: - $3 = HEAP32[(($9 << 2) + $5 | 0) + -128 >> 2]; - $4 = $3; - $7 = $3 >> 31; - $3 = HEAP32[$2 + 124 >> 2]; - $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); - $3 = i64toi32_i32$HIGH_BITS; - case 18: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -124 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 120 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 17: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -120 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 116 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 16: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -116 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 112 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 15: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -112 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 108 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 14: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -108 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 104 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 13: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -104 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 100 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 12: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -100 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 96 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 11: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -96 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 92 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 10: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -92 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 88 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 9: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -88 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 84 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 8: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -84 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 80 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 7: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -80 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 76 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 6: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -76 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 72 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 5: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -72 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 68 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 4: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -68 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 64 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 3: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -64 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 60 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 2: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -60 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 56 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 1: - $7 = HEAP32[(($9 << 2) + $5 | 0) + -56 >> 2]; - $6 = $7; - $8 = $7 >> 31; - $7 = HEAP32[$2 + 52 >> 2]; - $7 = __wasm_i64_mul($6, $8, $7, $7 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $7 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $7; - $3 = $6; - case 0: - $7 = ($9 << 2) + $5 | 0; - $8 = HEAP32[$7 + -52 >> 2]; - $6 = $8; - $10 = $8 >> 31; - $8 = HEAP32[$2 + 48 >> 2]; - $8 = __wasm_i64_mul($6, $10, $8, $8 >> 31) + $4 | 0; - $6 = $3 + i64toi32_i32$HIGH_BITS | 0; - $6 = $8 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $3 = HEAP32[$7 + -48 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 44 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $4 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -44 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 40 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -40 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 36 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -36 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 32 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -32 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 28 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -28 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 24 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -24 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 20 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -20 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 16 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -16 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 12 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -12 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 8 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -8 >> 2]; - $4 = $3; - $10 = $3 >> 31; - $3 = HEAP32[$2 + 4 >> 2]; - $4 = __wasm_i64_mul($4, $10, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = HEAP32[$7 + -4 >> 2]; - $4 = $3; - $7 = $3 >> 31; - $3 = HEAP32[$2 >> 2]; - $4 = __wasm_i64_mul($4, $7, $3, $3 >> 31); - $3 = $8 + $4 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $4 >>> 0 ? $6 + 1 | 0 : $6; - $4 = $3; - $3 = $6; - break; - default: - break label$4; - }; - } - $7 = $9 << 2; - $10 = $7 + $5 | 0; - $6 = HEAP32[$0 + $7 >> 2]; - $8 = $4; - $4 = $13; - $7 = $4 & 31; - HEAP32[$10 >> 2] = $6 + (32 <= ($4 & 63) >>> 0 ? $3 >> $7 : ((1 << $7) - 1 & $3) << 32 - $7 | $8 >>> $7); - $9 = $9 + 1 | 0; - if (($9 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 9) { - if ($3 >>> 0 >= 11) { - if (($3 | 0) != 12) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $16 = HEAP32[$5 + -40 >> 2]; - $15 = HEAP32[$5 + -44 >> 2]; - $6 = HEAP32[$2 >> 2]; - $17 = $6; - $25 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $26 = $6; - $27 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $24 = $6; - $29 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $30 = $6; - $22 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $31 = $6; - $32 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $28 = $6; - $34 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $35 = $6; - $21 = $6 >> 31; - $6 = HEAP32[$2 + 28 >> 2]; - $36 = $6; - $37 = $6 >> 31; - $6 = HEAP32[$2 + 32 >> 2]; - $33 = $6; - $39 = $6 >> 31; - $6 = HEAP32[$2 + 36 >> 2]; - $40 = $6; - $20 = $6 >> 31; - $2 = HEAP32[$2 + 40 >> 2]; - $41 = $2; - $42 = $2 >> 31; - $2 = 0; - while (1) { - $6 = $2 << 2; - $38 = $6 + $5 | 0; - $43 = HEAP32[$0 + $6 >> 2]; - $18 = $16; - $6 = __wasm_i64_mul($16, $16 >> 31, $40, $20); - $44 = i64toi32_i32$HIGH_BITS; - $16 = $14; - $19 = __wasm_i64_mul($15, $15 >> 31, $41, $42); - $15 = $19 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $44 | 0; - $6 = $15 >>> 0 < $19 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $15; - $15 = __wasm_i64_mul($14, $14 >> 31, $33, $39); - $14 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $14; - $14 = $11; - $19 = $15; - $15 = __wasm_i64_mul($11, $11 >> 31, $36, $37); - $11 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $11; - $11 = $10; - $10 = $15; - $15 = __wasm_i64_mul($11, $11 >> 31, $35, $21); - $10 = $10 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $10; - $10 = $12; - $19 = $15; - $15 = __wasm_i64_mul($12, $12 >> 31, $28, $34); - $12 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $12; - $12 = $8; - $19 = $15; - $15 = __wasm_i64_mul($8, $8 >> 31, $31, $32); - $8 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $8; - $8 = $7; - $19 = $15; - $15 = __wasm_i64_mul($7, $7 >> 31, $30, $22); - $7 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $7; - $7 = $13; - $15 = __wasm_i64_mul($7, $7 >> 31, $24, $29); - $13 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $15 = $13; - $13 = $3; - $23 = $38; - $19 = $15; - $15 = __wasm_i64_mul($3, $3 >> 31, $26, $27); - $3 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $3; - $3 = $9; - $15 = __wasm_i64_mul($3, $3 >> 31, $17, $25); - $9 = $19 + $15 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $15 >>> 0 ? $6 + 1 | 0 : $6; - $38 = $9; - $9 = $4; - $15 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $15 : ((1 << $15) - 1 & $6) << 32 - $15 | $38 >>> $15) + $43 | 0; - HEAP32[$23 >> 2] = $9; - $15 = $18; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $16 = HEAP32[$5 + -40 >> 2]; - $15 = HEAP32[$5 + -44 >> 2]; - $6 = HEAP32[$5 + -48 >> 2]; - $18 = HEAP32[$2 >> 2]; - $25 = $18; - $26 = $18 >> 31; - $18 = HEAP32[$2 + 4 >> 2]; - $27 = $18; - $24 = $18 >> 31; - $18 = HEAP32[$2 + 8 >> 2]; - $29 = $18; - $30 = $18 >> 31; - $18 = HEAP32[$2 + 12 >> 2]; - $22 = $18; - $31 = $18 >> 31; - $18 = HEAP32[$2 + 16 >> 2]; - $32 = $18; - $28 = $18 >> 31; - $18 = HEAP32[$2 + 20 >> 2]; - $34 = $18; - $35 = $18 >> 31; - $18 = HEAP32[$2 + 24 >> 2]; - $21 = $18; - $36 = $18 >> 31; - $18 = HEAP32[$2 + 28 >> 2]; - $37 = $18; - $33 = $18 >> 31; - $18 = HEAP32[$2 + 32 >> 2]; - $39 = $18; - $40 = $18 >> 31; - $18 = HEAP32[$2 + 36 >> 2]; - $20 = $18; - $41 = $18 >> 31; - $18 = HEAP32[$2 + 40 >> 2]; - $42 = $18; - $38 = $18 >> 31; - $2 = HEAP32[$2 + 44 >> 2]; - $43 = $2; - $44 = $2 >> 31; - $2 = 0; - while (1) { - $18 = $2 << 2; - $19 = $18 + $5 | 0; - $46 = HEAP32[$0 + $18 >> 2]; - $18 = $15; - $17 = __wasm_i64_mul($15, $15 >> 31, $42, $38); - $23 = i64toi32_i32$HIGH_BITS; - $15 = $16; - $45 = __wasm_i64_mul($6, $6 >> 31, $43, $44); - $17 = $45 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $23 | 0; - $6 = $17 >>> 0 < $45 >>> 0 ? $6 + 1 | 0 : $6; - $23 = $17; - $17 = __wasm_i64_mul($16, $16 >> 31, $20, $41); - $16 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $16 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $16; - $16 = $14; - $23 = $17; - $17 = __wasm_i64_mul($14, $14 >> 31, $39, $40); - $14 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $14 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $14; - $14 = $11; - $23 = $17; - $17 = __wasm_i64_mul($11, $11 >> 31, $37, $33); - $11 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $11; - $11 = $10; - $10 = $17; - $17 = __wasm_i64_mul($11, $11 >> 31, $21, $36); - $10 = $10 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $10; - $10 = $12; - $23 = $17; - $17 = __wasm_i64_mul($12, $12 >> 31, $34, $35); - $12 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $12; - $12 = $8; - $23 = $17; - $17 = __wasm_i64_mul($8, $8 >> 31, $32, $28); - $8 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $8; - $8 = $7; - $23 = $17; - $17 = __wasm_i64_mul($7, $7 >> 31, $22, $31); - $7 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $23 = $7; - $7 = $13; - $17 = __wasm_i64_mul($7, $7 >> 31, $29, $30); - $13 = $23 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $17 = $13; - $13 = $3; - $23 = $19; - $19 = $17; - $17 = __wasm_i64_mul($3, $3 >> 31, $27, $24); - $3 = $19 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $3; - $3 = $9; - $17 = __wasm_i64_mul($3, $3 >> 31, $25, $26); - $9 = $19 + $17 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $17 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $9; - $9 = $4; - $17 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $17 : ((1 << $17) - 1 & $6) << 32 - $17 | $19 >>> $17) + $46 | 0; - HEAP32[$23 >> 2] = $9; - $6 = $18; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 10) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $6 = HEAP32[$2 >> 2]; - $15 = $6; - $18 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $17 = $6; - $25 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $26 = $6; - $27 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $24 = $6; - $29 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $30 = $6; - $22 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $31 = $6; - $32 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $28 = $6; - $34 = $6 >> 31; - $6 = HEAP32[$2 + 28 >> 2]; - $35 = $6; - $21 = $6 >> 31; - $2 = HEAP32[$2 + 32 >> 2]; - $36 = $2; - $37 = $2 >> 31; - $2 = 0; - while (1) { - $6 = $2 << 2; - $33 = $6 + $5 | 0; - $39 = HEAP32[$0 + $6 >> 2]; - $16 = $11; - $6 = __wasm_i64_mul($11, $11 >> 31, $35, $21); - $40 = i64toi32_i32$HIGH_BITS; - $11 = $10; - $20 = __wasm_i64_mul($14, $14 >> 31, $36, $37); - $14 = $20 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $40 | 0; - $6 = $14 >>> 0 < $20 >>> 0 ? $6 + 1 | 0 : $6; - $10 = $14; - $14 = __wasm_i64_mul($11, $11 >> 31, $28, $34); - $10 = $10 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = $10; - $10 = $12; - $20 = $14; - $14 = __wasm_i64_mul($12, $12 >> 31, $31, $32); - $12 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = $12; - $12 = $8; - $20 = $14; - $14 = __wasm_i64_mul($8, $8 >> 31, $30, $22); - $8 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = $8; - $8 = $7; - $20 = $14; - $14 = __wasm_i64_mul($7, $7 >> 31, $24, $29); - $7 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $7; - $7 = $13; - $14 = __wasm_i64_mul($7, $7 >> 31, $26, $27); - $13 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $14 = $13; - $13 = $3; - $19 = $33; - $20 = $14; - $14 = __wasm_i64_mul($3, $3 >> 31, $17, $25); - $3 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $3; - $3 = $9; - $14 = __wasm_i64_mul($3, $3 >> 31, $15, $18); - $9 = $20 + $14 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $14 >>> 0 ? $6 + 1 | 0 : $6; - $33 = $9; - $9 = $4; - $14 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $14 : ((1 << $14) - 1 & $6) << 32 - $14 | $33 >>> $14) + $39 | 0; - HEAP32[$19 >> 2] = $9; - $14 = $16; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$5 + -32 >> 2]; - $14 = HEAP32[$5 + -36 >> 2]; - $16 = HEAP32[$5 + -40 >> 2]; - $6 = HEAP32[$2 >> 2]; - $18 = $6; - $17 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $25 = $6; - $26 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $27 = $6; - $24 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $29 = $6; - $30 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $22 = $6; - $31 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $32 = $6; - $28 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $34 = $6; - $35 = $6 >> 31; - $6 = HEAP32[$2 + 28 >> 2]; - $21 = $6; - $36 = $6 >> 31; - $6 = HEAP32[$2 + 32 >> 2]; - $37 = $6; - $33 = $6 >> 31; - $2 = HEAP32[$2 + 36 >> 2]; - $39 = $2; - $40 = $2 >> 31; - $2 = 0; - while (1) { - $6 = $2 << 2; - $20 = $6 + $5 | 0; - $41 = HEAP32[$0 + $6 >> 2]; - $15 = $14; - $6 = __wasm_i64_mul($14, $14 >> 31, $37, $33); - $42 = i64toi32_i32$HIGH_BITS; - $14 = $11; - $38 = __wasm_i64_mul($16, $16 >> 31, $39, $40); - $16 = $38 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $42 | 0; - $6 = $16 >>> 0 < $38 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $16; - $16 = __wasm_i64_mul($11, $11 >> 31, $21, $36); - $11 = $19 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $11 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $11; - $11 = $10; - $10 = $16; - $16 = __wasm_i64_mul($11, $11 >> 31, $34, $35); - $10 = $10 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $10 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $10; - $10 = $12; - $19 = $16; - $16 = __wasm_i64_mul($12, $12 >> 31, $32, $28); - $12 = $19 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $12; - $12 = $8; - $19 = $16; - $16 = __wasm_i64_mul($8, $8 >> 31, $22, $31); - $8 = $19 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $8; - $8 = $7; - $19 = $16; - $16 = __wasm_i64_mul($7, $7 >> 31, $29, $30); - $7 = $19 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $19 = $7; - $7 = $13; - $16 = __wasm_i64_mul($7, $7 >> 31, $27, $24); - $13 = $19 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $16 = $13; - $13 = $3; - $19 = $20; - $20 = $16; - $16 = __wasm_i64_mul($3, $3 >> 31, $25, $26); - $3 = $20 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $3; - $3 = $9; - $16 = __wasm_i64_mul($3, $3 >> 31, $18, $17); - $9 = $20 + $16 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $16 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $9; - $9 = $4; - $16 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $16 : ((1 << $16) - 1 & $6) << 32 - $16 | $20 >>> $16) + $41 | 0; - HEAP32[$19 >> 2] = $9; - $16 = $15; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 5) { - if ($3 >>> 0 >= 7) { - if (($3 | 0) != 8) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$2 >> 2]; - $14 = $11; - $16 = $11 >> 31; - $11 = HEAP32[$2 + 4 >> 2]; - $15 = $11; - $18 = $11 >> 31; - $11 = HEAP32[$2 + 8 >> 2]; - $17 = $11; - $25 = $11 >> 31; - $11 = HEAP32[$2 + 12 >> 2]; - $26 = $11; - $27 = $11 >> 31; - $11 = HEAP32[$2 + 16 >> 2]; - $24 = $11; - $29 = $11 >> 31; - $11 = HEAP32[$2 + 20 >> 2]; - $30 = $11; - $22 = $11 >> 31; - $2 = HEAP32[$2 + 24 >> 2]; - $31 = $2; - $32 = $2 >> 31; - $2 = 0; - while (1) { - $11 = $2 << 2; - $28 = $11 + $5 | 0; - $34 = HEAP32[$0 + $11 >> 2]; - $11 = $12; - $6 = __wasm_i64_mul($11, $11 >> 31, $30, $22); - $35 = i64toi32_i32$HIGH_BITS; - $12 = $8; - $21 = __wasm_i64_mul($10, $10 >> 31, $31, $32); - $10 = $21 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $35 | 0; - $6 = $10 >>> 0 < $21 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $10; - $10 = __wasm_i64_mul($8, $8 >> 31, $24, $29); - $8 = $21 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = $8; - $8 = $7; - $21 = $10; - $10 = __wasm_i64_mul($7, $7 >> 31, $26, $27); - $7 = $21 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $7; - $7 = $13; - $10 = __wasm_i64_mul($7, $7 >> 31, $17, $25); - $13 = $21 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $10 = $13; - $13 = $3; - $20 = $28; - $21 = $10; - $10 = __wasm_i64_mul($3, $3 >> 31, $15, $18); - $3 = $21 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $3; - $3 = $9; - $10 = __wasm_i64_mul($3, $3 >> 31, $14, $16); - $9 = $21 + $10 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $10 >>> 0 ? $6 + 1 | 0 : $6; - $28 = $9; - $9 = $4; - $10 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $10 : ((1 << $10) - 1 & $6) << 32 - $10 | $28 >>> $10) + $34 | 0; - HEAP32[$20 >> 2] = $9; - $10 = $11; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$5 + -28 >> 2]; - $11 = HEAP32[$5 + -32 >> 2]; - $6 = HEAP32[$2 >> 2]; - $16 = $6; - $15 = $6 >> 31; - $6 = HEAP32[$2 + 4 >> 2]; - $18 = $6; - $17 = $6 >> 31; - $6 = HEAP32[$2 + 8 >> 2]; - $25 = $6; - $26 = $6 >> 31; - $6 = HEAP32[$2 + 12 >> 2]; - $27 = $6; - $24 = $6 >> 31; - $6 = HEAP32[$2 + 16 >> 2]; - $29 = $6; - $30 = $6 >> 31; - $6 = HEAP32[$2 + 20 >> 2]; - $22 = $6; - $31 = $6 >> 31; - $6 = HEAP32[$2 + 24 >> 2]; - $32 = $6; - $28 = $6 >> 31; - $2 = HEAP32[$2 + 28 >> 2]; - $34 = $2; - $35 = $2 >> 31; - $2 = 0; - while (1) { - $6 = $2 << 2; - $21 = $6 + $5 | 0; - $36 = HEAP32[$0 + $6 >> 2]; - $14 = $10; - $6 = __wasm_i64_mul($10, $10 >> 31, $32, $28); - $37 = i64toi32_i32$HIGH_BITS; - $10 = $12; - $33 = __wasm_i64_mul($11, $11 >> 31, $34, $35); - $11 = $33 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $37 | 0; - $6 = $11 >>> 0 < $33 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $11; - $11 = __wasm_i64_mul($12, $12 >> 31, $22, $31); - $12 = $20 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = $12; - $12 = $8; - $20 = $11; - $11 = __wasm_i64_mul($8, $8 >> 31, $29, $30); - $8 = $20 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = $8; - $8 = $7; - $20 = $11; - $11 = __wasm_i64_mul($7, $7 >> 31, $27, $24); - $7 = $20 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $20 = $7; - $7 = $13; - $11 = __wasm_i64_mul($7, $7 >> 31, $25, $26); - $13 = $20 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $11 = $13; - $13 = $3; - $20 = $21; - $21 = $11; - $11 = __wasm_i64_mul($3, $3 >> 31, $18, $17); - $3 = $21 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $3; - $3 = $9; - $11 = __wasm_i64_mul($3, $3 >> 31, $16, $15); - $9 = $21 + $11 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $9 >>> 0 < $11 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $9; - $9 = $4; - $11 = $9 & 31; - $9 = (32 <= ($9 & 63) >>> 0 ? $6 >> $11 : ((1 << $11) - 1 & $6) << 32 - $11 | $21 >>> $11) + $36 | 0; - HEAP32[$20 >> 2] = $9; - $11 = $14; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 6) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$2 >> 2]; - $10 = $12; - $11 = $12 >> 31; - $12 = HEAP32[$2 + 4 >> 2]; - $14 = $12; - $16 = $12 >> 31; - $12 = HEAP32[$2 + 8 >> 2]; - $15 = $12; - $18 = $12 >> 31; - $12 = HEAP32[$2 + 12 >> 2]; - $17 = $12; - $25 = $12 >> 31; - $2 = HEAP32[$2 + 16 >> 2]; - $26 = $2; - $27 = $2 >> 31; - $2 = 0; - while (1) { - $12 = $2 << 2; - $24 = $12 + $5 | 0; - $29 = HEAP32[$0 + $12 >> 2]; - $12 = $7; - $6 = __wasm_i64_mul($7, $7 >> 31, $17, $25); - $30 = i64toi32_i32$HIGH_BITS; - $7 = $13; - $22 = __wasm_i64_mul($8, $8 >> 31, $26, $27); - $8 = $22 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $30 | 0; - $6 = $8 >>> 0 < $22 >>> 0 ? $6 + 1 | 0 : $6; - $13 = $8; - $8 = __wasm_i64_mul($7, $7 >> 31, $15, $18); - $13 = $13 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $13; - $13 = $3; - $22 = $8; - $8 = __wasm_i64_mul($3, $3 >> 31, $14, $16); - $3 = $22 + $8 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = $9; - $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); - $8 = $8 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $8 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $4 & 31; - $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $8 >>> $9) + $29 | 0; - HEAP32[$24 >> 2] = $9; - $8 = $12; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$5 + -20 >> 2]; - $12 = HEAP32[$5 + -24 >> 2]; - $10 = HEAP32[$2 >> 2]; - $11 = $10; - $14 = $11 >> 31; - $10 = HEAP32[$2 + 4 >> 2]; - $16 = $10; - $15 = $10 >> 31; - $10 = HEAP32[$2 + 8 >> 2]; - $18 = $10; - $17 = $10 >> 31; - $10 = HEAP32[$2 + 12 >> 2]; - $25 = $10; - $26 = $10 >> 31; - $10 = HEAP32[$2 + 16 >> 2]; - $27 = $10; - $24 = $10 >> 31; - $2 = HEAP32[$2 + 20 >> 2]; - $29 = $2; - $30 = $2 >> 31; - $2 = 0; - while (1) { - $10 = $2 << 2; - $22 = $10 + $5 | 0; - $31 = HEAP32[$0 + $10 >> 2]; - $10 = $8; - $6 = __wasm_i64_mul($8, $8 >> 31, $27, $24); - $32 = i64toi32_i32$HIGH_BITS; - $8 = $7; - $28 = __wasm_i64_mul($12, $12 >> 31, $29, $30); - $12 = $28 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $32 | 0; - $6 = $12 >>> 0 < $28 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $12; - $12 = __wasm_i64_mul($7, $7 >> 31, $25, $26); - $7 = $21 + $12 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $21 = $7; - $7 = $13; - $12 = __wasm_i64_mul($7, $7 >> 31, $18, $17); - $13 = $21 + $12 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $12 = $13; - $13 = $3; - $21 = $22; - $22 = $12; - $12 = __wasm_i64_mul($3, $3 >> 31, $16, $15); - $3 = $22 + $12 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $12 >>> 0 ? $6 + 1 | 0 : $6; - $12 = $3; - $3 = $9; - $9 = __wasm_i64_mul($3, $3 >> 31, $11, $14); - $12 = $12 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $12 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $4 & 31; - $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $9 : ((1 << $9) - 1 & $6) << 32 - $9 | $12 >>> $9) + $31 | 0; - HEAP32[$21 >> 2] = $9; - $12 = $10; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if ($3 >>> 0 >= 3) { - if (($3 | 0) != 4) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$2 >> 2]; - $12 = $7; - $10 = $7 >> 31; - $7 = HEAP32[$2 + 4 >> 2]; - $11 = $7; - $14 = $7 >> 31; - $2 = HEAP32[$2 + 8 >> 2]; - $16 = $2; - $15 = $2 >> 31; - $2 = 0; - while (1) { - $7 = $2 << 2; - $8 = $7 + $5 | 0; - $18 = HEAP32[$0 + $7 >> 2]; - $7 = $3; - $3 = __wasm_i64_mul($7, $7 >> 31, $11, $14); - $6 = i64toi32_i32$HIGH_BITS; - $17 = $8; - $13 = __wasm_i64_mul($13, $13 >> 31, $16, $15); - $3 = $13 + $3 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $13 >>> 0 ? $6 + 1 | 0 : $6; - $8 = $3; - $3 = $9; - $9 = __wasm_i64_mul($3, $3 >> 31, $12, $10); - $13 = $8 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $13 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $13; - $8 = $4 & 31; - $9 = (32 <= ($4 & 63) >>> 0 ? $6 >> $8 : ((1 << $8) - 1 & $6) << 32 - $8 | $9 >>> $8) + $18 | 0; - HEAP32[$17 >> 2] = $9; - $13 = $7; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$5 + -12 >> 2]; - $7 = HEAP32[$5 + -16 >> 2]; - $8 = HEAP32[$2 >> 2]; - $10 = $8; - $11 = $8 >> 31; - $8 = HEAP32[$2 + 4 >> 2]; - $14 = $8; - $16 = $8 >> 31; - $8 = HEAP32[$2 + 8 >> 2]; - $15 = $8; - $18 = $8 >> 31; - $2 = HEAP32[$2 + 12 >> 2]; - $17 = $2; - $25 = $2 >> 31; - $2 = 0; - while (1) { - $8 = $2 << 2; - $12 = $8 + $5 | 0; - $26 = HEAP32[$0 + $8 >> 2]; - $8 = $13; - $6 = __wasm_i64_mul($8, $8 >> 31, $15, $18); - $27 = i64toi32_i32$HIGH_BITS; - $13 = $3; - $22 = $12; - $24 = __wasm_i64_mul($7, $7 >> 31, $17, $25); - $7 = $24 + $6 | 0; - $6 = i64toi32_i32$HIGH_BITS + $27 | 0; - $6 = $7 >>> 0 < $24 >>> 0 ? $6 + 1 | 0 : $6; - $12 = $7; - $7 = __wasm_i64_mul($3, $3 >> 31, $14, $16); - $3 = $12 + $7 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6; - $7 = $3; - $3 = $9; - $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); - $7 = $7 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $7 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $7; - $7 = $4; - $12 = $7 & 31; - $9 = (32 <= ($7 & 63) >>> 0 ? $6 >> $12 : ((1 << $12) - 1 & $6) << 32 - $12 | $9 >>> $12) + $26 | 0; - HEAP32[$22 >> 2] = $9; - $7 = $8; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($3 | 0) != 2) { - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $2 = HEAP32[$2 >> 2]; - $8 = $2; - $12 = $2 >> 31; - $2 = 0; - while (1) { - $3 = $2 << 2; - $10 = $3 + $5 | 0; - $6 = HEAP32[$0 + $3 >> 2]; - $9 = __wasm_i64_mul($9, $9 >> 31, $8, $12); - $7 = i64toi32_i32$HIGH_BITS; - $3 = $4; - $13 = $3 & 31; - $9 = $6 + (32 <= ($3 & 63) >>> 0 ? $7 >> $13 : ((1 << $13) - 1 & $7) << 32 - $13 | $9 >>> $13) | 0; - HEAP32[$10 >> 2] = $9; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (($1 | 0) < 1) { - break label$1 - } - $9 = HEAP32[$5 + -4 >> 2]; - $3 = HEAP32[$5 + -8 >> 2]; - $13 = HEAP32[$2 >> 2]; - $8 = $13; - $12 = $8 >> 31; - $2 = HEAP32[$2 + 4 >> 2]; - $10 = $2; - $11 = $2 >> 31; - $2 = 0; - while (1) { - $13 = $2 << 2; - $7 = $13 + $5 | 0; - $14 = HEAP32[$0 + $13 >> 2]; - $13 = $9; - $9 = __wasm_i64_mul($9, $9 >> 31, $8, $12); - $6 = i64toi32_i32$HIGH_BITS; - $15 = $7; - $7 = $9; - $9 = __wasm_i64_mul($3, $3 >> 31, $10, $11); - $3 = $7 + $9 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $6 = $3 >>> 0 < $9 >>> 0 ? $6 + 1 | 0 : $6; - $9 = $3; - $3 = $4; - $7 = $3 & 31; - $9 = (32 <= ($3 & 63) >>> 0 ? $6 >> $7 : ((1 << $7) - 1 & $6) << 32 - $7 | $9 >>> $7) + $14 | 0; - HEAP32[$15 >> 2] = $9; - $3 = $13; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__lpc_compute_expected_bits_per_residual_sample($0, $1) { - if (!!($0 > 0.0)) { - $0 = log(.5 / +($1 >>> 0) * $0) * .5 / .6931471805599453; - return $0 >= 0.0 ? $0 : 0.0; - } - return $0 < 0.0 ? 1.e+32 : 0.0; - } - - function FLAC__lpc_compute_best_order($0, $1, $2, $3) { - var $4 = 0.0, $5 = 0, $6 = 0, $7 = 0.0, $8 = 0, $9 = 0, $10 = 0.0; - $5 = 1; - if ($1) { - $10 = .5 / +($2 >>> 0); - $7 = 4294967295.0; - while (1) { - $4 = HEAPF64[($6 << 3) + $0 >> 3]; - label$3 : { - if (!!($4 > 0.0)) { - $4 = log($10 * $4) * .5 / .6931471805599453; - $4 = $4 >= 0.0 ? $4 : 0.0; - break label$3; - } - $4 = $4 < 0.0 ? 1.e+32 : 0.0; - } - $4 = $4 * +($2 - $5 >>> 0) + +(Math_imul($3, $5) >>> 0); - $8 = $4 < $7; - $7 = $8 ? $4 : $7; - $9 = $8 ? $6 : $9; - $5 = $5 + 1 | 0; - $6 = $6 + 1 | 0; - if (($6 | 0) != ($1 | 0)) { - continue - } - break; - }; - $0 = $9 + 1 | 0; - } else { - $0 = 1 - } - return $0; - } - - function strlen($0) { - var $1 = 0, $2 = 0, $3 = 0; - label$1 : { - label$2 : { - $1 = $0; - if (!($1 & 3)) { - break label$2 - } - if (!HEAPU8[$0 | 0]) { - return 0 - } - while (1) { - $1 = $1 + 1 | 0; - if (!($1 & 3)) { - break label$2 - } - if (HEAPU8[$1 | 0]) { - continue - } - break; - }; - break label$1; - } - while (1) { - $2 = $1; - $1 = $1 + 4 | 0; - $3 = HEAP32[$2 >> 2]; - if (!(($3 ^ -1) & $3 + -16843009 & -2139062144)) { - continue - } - break; - }; - if (!($3 & 255)) { - return $2 - $0 | 0 - } - while (1) { - $3 = HEAPU8[$2 + 1 | 0]; - $1 = $2 + 1 | 0; - $2 = $1; - if ($3) { - continue - } - break; - }; - } - return $1 - $0 | 0; - } - - function __strchrnul($0, $1) { - var $2 = 0, $3 = 0; - label$1 : { - $3 = $1 & 255; - if ($3) { - if ($0 & 3) { - while (1) { - $2 = HEAPU8[$0 | 0]; - if (!$2 | ($2 | 0) == ($1 & 255)) { - break label$1 - } - $0 = $0 + 1 | 0; - if ($0 & 3) { - continue - } - break; - } - } - $2 = HEAP32[$0 >> 2]; - label$5 : { - if (($2 ^ -1) & $2 + -16843009 & -2139062144) { - break label$5 - } - $3 = Math_imul($3, 16843009); - while (1) { - $2 = $2 ^ $3; - if (($2 ^ -1) & $2 + -16843009 & -2139062144) { - break label$5 - } - $2 = HEAP32[$0 + 4 >> 2]; - $0 = $0 + 4 | 0; - if (!($2 + -16843009 & ($2 ^ -1) & -2139062144)) { - continue - } - break; - }; - } - while (1) { - $2 = $0; - $3 = HEAPU8[$2 | 0]; - if ($3) { - $0 = $2 + 1 | 0; - if (($3 | 0) != ($1 & 255)) { - continue - } - } - break; - }; - return $2; - } - return strlen($0) + $0 | 0; - } - return $0; - } - - function strchr($0, $1) { - $0 = __strchrnul($0, $1); - return HEAPU8[$0 | 0] == ($1 & 255) ? $0 : 0; - } - - function __stdio_write($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $3 = global$0 - 32 | 0; - global$0 = $3; - $4 = HEAP32[$0 + 28 >> 2]; - HEAP32[$3 + 16 >> 2] = $4; - $5 = HEAP32[$0 + 20 >> 2]; - HEAP32[$3 + 28 >> 2] = $2; - HEAP32[$3 + 24 >> 2] = $1; - $1 = $5 - $4 | 0; - HEAP32[$3 + 20 >> 2] = $1; - $4 = $1 + $2 | 0; - $9 = 2; - $1 = $3 + 16 | 0; - label$1 : { - label$2 : { - label$3 : { - if (!__wasi_syscall_ret(__wasi_fd_write(HEAP32[$0 + 60 >> 2], $3 + 16 | 0, 2, $3 + 12 | 0) | 0)) { - while (1) { - $5 = HEAP32[$3 + 12 >> 2]; - if (($5 | 0) == ($4 | 0)) { - break label$3 - } - if (($5 | 0) <= -1) { - break label$2 - } - $6 = HEAP32[$1 + 4 >> 2]; - $7 = $5 >>> 0 > $6 >>> 0; - $8 = ($7 << 3) + $1 | 0; - $6 = $5 - ($7 ? $6 : 0) | 0; - HEAP32[$8 >> 2] = $6 + HEAP32[$8 >> 2]; - $8 = ($7 ? 12 : 4) + $1 | 0; - HEAP32[$8 >> 2] = HEAP32[$8 >> 2] - $6; - $4 = $4 - $5 | 0; - $1 = $7 ? $1 + 8 | 0 : $1; - $9 = $9 - $7 | 0; - if (!__wasi_syscall_ret(__wasi_fd_write(HEAP32[$0 + 60 >> 2], $1 | 0, $9 | 0, $3 + 12 | 0) | 0)) { - continue - } - break; - } - } - HEAP32[$3 + 12 >> 2] = -1; - if (($4 | 0) != -1) { - break label$2 - } - } - $1 = HEAP32[$0 + 44 >> 2]; - HEAP32[$0 + 28 >> 2] = $1; - HEAP32[$0 + 20 >> 2] = $1; - HEAP32[$0 + 16 >> 2] = $1 + HEAP32[$0 + 48 >> 2]; - $0 = $2; - break label$1; - } - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 >> 2] = HEAP32[$0 >> 2] | 32; - $0 = 0; - if (($9 | 0) == 2) { - break label$1 - } - $0 = $2 - HEAP32[$1 + 4 >> 2] | 0; - } - global$0 = $3 + 32 | 0; - return $0 | 0; - } - - function FLAC__memory_alloc_aligned_int32_array($0, $1, $2) { - var $3 = 0; - label$1 : { - if ($0 >>> 0 > 1073741823) { - break label$1 - } - $0 = dlmalloc($0 ? $0 << 2 : 1); - if (!$0) { - break label$1 - } - $3 = HEAP32[$1 >> 2]; - if ($3) { - dlfree($3) - } - HEAP32[$1 >> 2] = $0; - HEAP32[$2 >> 2] = $0; - $3 = 1; - } - return $3; - } - - function FLAC__memory_alloc_aligned_uint64_array($0, $1, $2) { - var $3 = 0; - label$1 : { - if ($0 >>> 0 > 536870911) { - break label$1 - } - $0 = dlmalloc($0 ? $0 << 3 : 1); - if (!$0) { - break label$1 - } - $3 = HEAP32[$1 >> 2]; - if ($3) { - dlfree($3) - } - HEAP32[$1 >> 2] = $0; - HEAP32[$2 >> 2] = $0; - $3 = 1; - } - return $3; - } - - function safe_malloc_mul_2op_p($0, $1) { - if (!($1 ? $0 : 0)) { - return dlmalloc(1) - } - __wasm_i64_mul($1, 0, $0, 0); - if (i64toi32_i32$HIGH_BITS) { - $0 = 0 - } else { - $0 = dlmalloc(Math_imul($0, $1)) - } - return $0; - } - - function FLAC__fixed_compute_best_predictor($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = Math_fround(0), $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if ($1) { - $3 = HEAP32[$0 + -4 >> 2]; - $8 = HEAP32[$0 + -8 >> 2]; - $12 = $3 - $8 | 0; - $5 = HEAP32[$0 + -12 >> 2]; - $9 = $12 + ($5 - $8 | 0) | 0; - $17 = $9 + ((($5 << 1) - $8 | 0) - HEAP32[$0 + -16 >> 2] | 0) | 0; - while (1) { - $8 = HEAP32[($15 << 2) + $0 >> 2]; - $5 = $8 >> 31; - $14 = ($5 ^ $5 + $8) + $14 | 0; - $5 = $8 - $3 | 0; - $11 = $5 >> 31; - $13 = ($11 ^ $5 + $11) + $13 | 0; - $11 = $5 - $12 | 0; - $3 = $11 >> 31; - $10 = ($3 ^ $3 + $11) + $10 | 0; - $9 = $11 - $9 | 0; - $3 = $9 >> 31; - $6 = ($3 ^ $3 + $9) + $6 | 0; - $12 = $9 - $17 | 0; - $3 = $12 >> 31; - $7 = ($3 ^ $3 + $12) + $7 | 0; - $3 = $8; - $12 = $5; - $17 = $9; - $9 = $11; - $15 = $15 + 1 | 0; - if (($15 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - $0 = $13 >>> 0 < $10 >>> 0 ? $13 : $10; - $0 = $0 >>> 0 < $6 >>> 0 ? $0 : $6; - label$3 : { - if ($14 >>> 0 < ($0 >>> 0 < $7 >>> 0 ? $0 : $7) >>> 0) { - break label$3 - } - $16 = 1; - $0 = $10 >>> 0 < $6 >>> 0 ? $10 : $6; - if ($13 >>> 0 < ($0 >>> 0 < $7 >>> 0 ? $0 : $7) >>> 0) { - break label$3 - } - $0 = $6 >>> 0 < $7 >>> 0; - $16 = $10 >>> 0 < ($0 ? $6 : $7) >>> 0 ? 2 : $0 ? 3 : 4; - } - $0 = $2; - if ($14) { - $4 = Math_fround(log(+($14 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $4 = Math_fround(0.0) - } - HEAPF32[$0 >> 2] = $4; - $0 = $2; - if ($13) { - $4 = Math_fround(log(+($13 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $4 = Math_fround(0.0) - } - HEAPF32[$0 + 4 >> 2] = $4; - $0 = $2; - if ($10) { - $4 = Math_fround(log(+($10 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $4 = Math_fround(0.0) - } - HEAPF32[$0 + 8 >> 2] = $4; - $0 = $2; - if ($6) { - $4 = Math_fround(log(+($6 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $4 = Math_fround(0.0) - } - HEAPF32[$0 + 12 >> 2] = $4; - if (!$7) { - HEAPF32[$2 + 16 >> 2] = 0; - return $16 | 0; - } - (wasm2js_i32$0 = $2, wasm2js_f32$0 = Math_fround(log(+($7 >>> 0) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453)), HEAPF32[wasm2js_i32$0 + 16 >> 2] = wasm2js_f32$0; - return $16 | 0; - } - - function FLAC__fixed_compute_best_predictor_wide($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = Math_fround(0), $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - label$1 : { - if (!$1) { - break label$1 - } - $5 = HEAP32[$0 + -4 >> 2]; - $8 = HEAP32[$0 + -8 >> 2]; - $6 = $5 - $8 | 0; - $9 = HEAP32[$0 + -12 >> 2]; - $14 = $6 + ($9 - $8 | 0) | 0; - $21 = $14 + ((($9 << 1) - $8 | 0) - HEAP32[$0 + -16 >> 2] | 0) | 0; - $9 = 0; - $8 = 0; - while (1) { - $3 = HEAP32[($20 << 2) + $0 >> 2]; - $4 = $3 >> 31; - $4 = $4 ^ $3 + $4; - $7 = $4 + $19 | 0; - if ($7 >>> 0 < $4 >>> 0) { - $18 = $18 + 1 | 0 - } - $19 = $7; - $4 = $3 - $5 | 0; - $7 = $4 >> 31; - $7 = $7 ^ $4 + $7; - $5 = $7 + $17 | 0; - if ($5 >>> 0 < $7 >>> 0) { - $15 = $15 + 1 | 0 - } - $17 = $5; - $7 = $4 - $6 | 0; - $5 = $7 >> 31; - $5 = $5 ^ $5 + $7; - $6 = $5 + $16 | 0; - if ($6 >>> 0 < $5 >>> 0) { - $10 = $10 + 1 | 0 - } - $16 = $6; - $14 = $7 - $14 | 0; - $5 = $14 >> 31; - $5 = $5 ^ $5 + $14; - $6 = $5 + $12 | 0; - if ($6 >>> 0 < $5 >>> 0) { - $8 = $8 + 1 | 0 - } - $12 = $6; - $6 = $14 - $21 | 0; - $5 = $6 >> 31; - $5 = $5 ^ $5 + $6; - $6 = $5 + $13 | 0; - if ($6 >>> 0 < $5 >>> 0) { - $9 = $9 + 1 | 0 - } - $13 = $6; - $5 = $3; - $6 = $4; - $21 = $14; - $14 = $7; - $20 = $20 + 1 | 0; - if (($20 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - $3 = ($10 | 0) == ($15 | 0) & $17 >>> 0 < $16 >>> 0 | $15 >>> 0 < $10 >>> 0; - $4 = $3 ? $17 : $16; - $0 = $4; - $3 = $3 ? $15 : $10; - $4 = ($8 | 0) == ($3 | 0) & $4 >>> 0 < $12 >>> 0 | $3 >>> 0 < $8 >>> 0; - $7 = $4 ? $0 : $12; - $3 = $4 ? $3 : $8; - $4 = ($9 | 0) == ($3 | 0) & $7 >>> 0 < $13 >>> 0 | $3 >>> 0 < $9 >>> 0; - $7 = $4 ? $7 : $13; - $3 = $4 ? $3 : $9; - $0 = 0; - label$4 : { - if (($3 | 0) == ($18 | 0) & $19 >>> 0 < $7 >>> 0 | $18 >>> 0 < $3 >>> 0) { - break label$4 - } - $3 = ($8 | 0) == ($10 | 0) & $16 >>> 0 < $12 >>> 0 | $10 >>> 0 < $8 >>> 0; - $4 = $3 ? $16 : $12; - $0 = $4; - $3 = $3 ? $10 : $8; - $4 = ($9 | 0) == ($3 | 0) & $4 >>> 0 < $13 >>> 0 | $3 >>> 0 < $9 >>> 0; - $7 = $4 ? $0 : $13; - $3 = $4 ? $3 : $9; - $0 = 1; - if (($3 | 0) == ($15 | 0) & $17 >>> 0 < $7 >>> 0 | $15 >>> 0 < $3 >>> 0) { - break label$4 - } - $0 = ($8 | 0) == ($9 | 0) & $12 >>> 0 < $13 >>> 0 | $8 >>> 0 < $9 >>> 0; - $3 = $0; - $4 = $3 ? $12 : $13; - $0 = $3 ? $8 : $9; - $0 = ($0 | 0) == ($10 | 0) & $16 >>> 0 < $4 >>> 0 | $10 >>> 0 < $0 >>> 0 ? 2 : $3 ? 3 : 4; - } - $6 = $2; - if ($18 | $19) { - $11 = Math_fround(log((+($19 >>> 0) + 4294967296.0 * +($18 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $11 = Math_fround(0.0) - } - HEAPF32[$6 >> 2] = $11; - $6 = $2; - if ($15 | $17) { - $11 = Math_fround(log((+($17 >>> 0) + 4294967296.0 * +($15 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $11 = Math_fround(0.0) - } - HEAPF32[$6 + 4 >> 2] = $11; - $6 = $2; - if ($10 | $16) { - $11 = Math_fround(log((+($16 >>> 0) + 4294967296.0 * +($10 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $11 = Math_fround(0.0) - } - HEAPF32[$6 + 8 >> 2] = $11; - $6 = $2; - if ($8 | $12) { - $11 = Math_fround(log((+($12 >>> 0) + 4294967296.0 * +($8 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453) - } else { - $11 = Math_fround(0.0) - } - HEAPF32[$6 + 12 >> 2] = $11; - if (!($9 | $13)) { - HEAPF32[$2 + 16 >> 2] = 0; - return $0 | 0; - } - (wasm2js_i32$0 = $2, wasm2js_f32$0 = Math_fround(log((+($13 >>> 0) + 4294967296.0 * +($9 >>> 0)) * .6931471805599453 / +($1 >>> 0)) / .6931471805599453)), HEAPF32[wasm2js_i32$0 + 16 >> 2] = wasm2js_f32$0; - return $0 | 0; - } - - function FLAC__fixed_compute_residual($0, $1, $2, $3) { - var $4 = 0, $5 = 0; - label$1 : { - label$2 : { - label$3 : { - switch ($2 | 0) { - case 4: - $2 = 0; - if (($1 | 0) <= 0) { - break label$2 - } - while (1) { - $5 = $2 << 2; - $4 = $5 + $0 | 0; - HEAP32[$3 + $5 >> 2] = (HEAP32[$4 + -16 >> 2] + (HEAP32[$4 >> 2] + Math_imul(HEAP32[$4 + -8 >> 2], 6) | 0) | 0) - (HEAP32[$4 + -12 >> 2] + HEAP32[$4 + -4 >> 2] << 2); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 3: - $2 = 0; - if (($1 | 0) <= 0) { - break label$2 - } - while (1) { - $5 = $2 << 2; - $4 = $5 + $0 | 0; - HEAP32[$3 + $5 >> 2] = (HEAP32[$4 >> 2] - HEAP32[$4 + -12 >> 2] | 0) + Math_imul(HEAP32[$4 + -8 >> 2] - HEAP32[$4 + -4 >> 2] | 0, 3); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 2: - $2 = 0; - if (($1 | 0) <= 0) { - break label$2 - } - while (1) { - $5 = $2 << 2; - $4 = $5 + $0 | 0; - HEAP32[$3 + $5 >> 2] = HEAP32[$4 + -8 >> 2] + (HEAP32[$4 >> 2] - (HEAP32[$4 + -4 >> 2] << 1) | 0); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 0: - break label$1; - case 1: - break label$3; - default: - break label$2; - }; - } - $2 = 0; - if (($1 | 0) <= 0) { - break label$2 - } - while (1) { - $5 = $2 << 2; - $4 = $5 + $0 | 0; - HEAP32[$3 + $5 >> 2] = HEAP32[$4 >> 2] - HEAP32[$4 + -4 >> 2]; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - return; - } - memcpy($3, $0, $1 << 2); - } - - function FLAC__fixed_restore_signal($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - label$1 : { - label$2 : { - label$3 : { - switch ($2 | 0) { - case 4: - if (($1 | 0) < 1) { - break label$2 - } - $5 = HEAP32[$3 + -12 >> 2]; - $6 = HEAP32[$3 + -4 >> 2]; - $2 = 0; - while (1) { - $8 = $2 << 2; - $7 = $8 + $3 | 0; - $4 = HEAP32[$7 + -8 >> 2]; - $6 = ((HEAP32[$0 + $8 >> 2] + Math_imul($4, -6) | 0) - HEAP32[$7 + -16 >> 2] | 0) + ($5 + $6 << 2) | 0; - HEAP32[$7 >> 2] = $6; - $5 = $4; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 3: - if (($1 | 0) < 1) { - break label$2 - } - $4 = HEAP32[$3 + -12 >> 2]; - $5 = HEAP32[$3 + -4 >> 2]; - $2 = 0; - while (1) { - $6 = $2 << 2; - $7 = $6 + $3 | 0; - $8 = HEAP32[$0 + $6 >> 2] + $4 | 0; - $4 = HEAP32[$7 + -8 >> 2]; - $5 = $8 + Math_imul($5 - $4 | 0, 3) | 0; - HEAP32[$7 >> 2] = $5; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 2: - if (($1 | 0) < 1) { - break label$2 - } - $4 = HEAP32[$3 + -4 >> 2]; - $2 = 0; - while (1) { - $5 = $2 << 2; - $6 = $5 + $3 | 0; - $4 = (HEAP32[$0 + $5 >> 2] + ($4 << 1) | 0) - HEAP32[$6 + -8 >> 2] | 0; - HEAP32[$6 >> 2] = $4; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$2; - case 0: - break label$1; - case 1: - break label$3; - default: - break label$2; - }; - } - if (($1 | 0) < 1) { - break label$2 - } - $4 = HEAP32[$3 + -4 >> 2]; - $2 = 0; - while (1) { - $5 = $2 << 2; - $4 = HEAP32[$5 + $0 >> 2] + $4 | 0; - HEAP32[$3 + $5 >> 2] = $4; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - return; - } - memcpy($3, $0, $1 << 2); - } - - function __toread($0) { - var $1 = 0, $2 = 0; - $1 = HEAPU8[$0 + 74 | 0]; - HEAP8[$0 + 74 | 0] = $1 + -1 | $1; - if (HEAPU32[$0 + 20 >> 2] > HEAPU32[$0 + 28 >> 2]) { - FUNCTION_TABLE[HEAP32[$0 + 36 >> 2]]($0, 0, 0) | 0 - } - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - $1 = HEAP32[$0 >> 2]; - if ($1 & 4) { - HEAP32[$0 >> 2] = $1 | 32; - return -1; - } - $2 = HEAP32[$0 + 44 >> 2] + HEAP32[$0 + 48 >> 2] | 0; - HEAP32[$0 + 8 >> 2] = $2; - HEAP32[$0 + 4 >> 2] = $2; - return $1 << 27 >> 31; - } - - function FLAC__stream_decoder_new() { - var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0; - $3 = dlcalloc(1, 8); - if ($3) { - $2 = dlcalloc(1, 504); - HEAP32[$3 >> 2] = $2; - if ($2) { - $0 = dlcalloc(1, 6160); - HEAP32[$3 + 4 >> 2] = $0; - if ($0) { - $1 = dlcalloc(1, 44); - HEAP32[$0 + 56 >> 2] = $1; - if ($1) { - HEAP32[$0 + 1128 >> 2] = 16; - $4 = dlmalloc(HEAP32[1364] << 1 & -16); - HEAP32[$0 + 1120 >> 2] = $4; - if ($4) { - HEAP32[$0 + 252 >> 2] = 0; - HEAP32[$0 + 220 >> 2] = 0; - HEAP32[$0 + 224 >> 2] = 0; - $1 = $0 + 3616 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - $1 = $0 + 3608 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - $1 = $0 + 3600 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - $1 = $0 + 3592 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - HEAP32[$0 + 60 >> 2] = 0; - HEAP32[$0 + 64 >> 2] = 0; - HEAP32[$0 + 68 >> 2] = 0; - HEAP32[$0 + 72 >> 2] = 0; - HEAP32[$0 + 76 >> 2] = 0; - HEAP32[$0 + 80 >> 2] = 0; - HEAP32[$0 + 84 >> 2] = 0; - HEAP32[$0 + 88 >> 2] = 0; - HEAP32[$0 + 92 >> 2] = 0; - HEAP32[$0 + 96 >> 2] = 0; - HEAP32[$0 + 100 >> 2] = 0; - HEAP32[$0 + 104 >> 2] = 0; - HEAP32[$0 + 108 >> 2] = 0; - HEAP32[$0 + 112 >> 2] = 0; - HEAP32[$0 + 116 >> 2] = 0; - HEAP32[$0 + 120 >> 2] = 0; - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 124 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 136 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 148 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 160 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 172 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 184 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 196 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init($0 + 208 | 0); - HEAP32[$0 + 48 >> 2] = 0; - HEAP32[$0 + 52 >> 2] = 0; - memset($0 + 608 | 0, 512); - HEAP32[$0 + 1124 >> 2] = 0; - HEAP32[$0 + 608 >> 2] = 1; - HEAP32[$0 + 32 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 0; - HEAP32[$0 + 28 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$2 + 28 >> 2] = 0; - FLAC__ogg_decoder_aspect_set_defaults($2 + 32 | 0); - HEAP32[$2 >> 2] = 9; - return $3 | 0; - } - FLAC__bitreader_delete($1); - } - dlfree($0); - } - dlfree($2); - } - dlfree($3); - } - return 0; - } - - function FLAC__stream_decoder_delete($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0; - if ($0) { - FLAC__stream_decoder_finish($0); - $1 = HEAP32[$0 + 4 >> 2]; - $2 = HEAP32[$1 + 1120 >> 2]; - if ($2) { - dlfree($2); - $1 = HEAP32[$0 + 4 >> 2]; - } - FLAC__bitreader_delete(HEAP32[$1 + 56 >> 2]); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 124 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 136 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 148 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 160 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 172 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 184 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 196 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 208 | 0); - dlfree(HEAP32[$0 + 4 >> 2]); - dlfree(HEAP32[$0 >> 2]); - dlfree($0); - } - } - - function FLAC__stream_decoder_finish($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0; - $3 = 1; - if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9) { - $1 = HEAP32[$0 + 4 >> 2]; - FLAC__MD5Final($1 + 3732 | 0, $1 + 3636 | 0); - dlfree(HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2]); - HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 252 >> 2] = 0; - FLAC__bitreader_free(HEAP32[$1 + 56 >> 2]); - $3 = $0 + 4 | 0; - $1 = HEAP32[$0 + 4 >> 2]; - $2 = HEAP32[$1 + 60 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 60 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3592 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 92 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3592 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 - -64 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] - -64 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3596 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 96 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3596 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 68 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 68 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3600 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 100 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3600 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 72 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 72 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3604 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 104 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3604 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 76 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 76 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3608 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 108 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3608 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 80 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 80 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3612 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 112 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3612 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 84 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 84 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3616 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 116 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3616 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 88 >> 2]; - if ($2) { - dlfree($2 + -16 | 0); - HEAP32[HEAP32[$3 >> 2] + 88 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - $2 = HEAP32[$1 + 3620 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$3 >> 2] + 120 >> 2] = 0; - HEAP32[HEAP32[$3 >> 2] + 3620 >> 2] = 0; - $1 = HEAP32[$3 >> 2]; - } - HEAP32[$1 + 220 >> 2] = 0; - HEAP32[$1 + 224 >> 2] = 0; - if (HEAP32[$1 >> 2]) { - $1 = HEAP32[$0 >> 2] + 32 | 0; - ogg_sync_clear($1 + 368 | 0); - ogg_stream_clear($1 + 8 | 0); - $1 = HEAP32[$0 + 4 >> 2]; - } - $2 = HEAP32[$1 + 52 >> 2]; - if ($2) { - if (($2 | 0) != HEAP32[1887]) { - fclose($2); - $1 = HEAP32[$3 >> 2]; - } - HEAP32[$1 + 52 >> 2] = 0; - } - $3 = 1; - if (HEAP32[$1 + 3624 >> 2]) { - $3 = !memcmp($1 + 312 | 0, $1 + 3732 | 0, 16) - } - HEAP32[$1 + 48 >> 2] = 0; - HEAP32[$1 + 3632 >> 2] = 0; - memset($1 + 608 | 0, 512); - HEAP32[$1 + 32 >> 2] = 0; - HEAP32[$1 + 24 >> 2] = 0; - HEAP32[$1 + 28 >> 2] = 0; - HEAP32[$1 + 16 >> 2] = 0; - HEAP32[$1 + 20 >> 2] = 0; - HEAP32[$1 + 8 >> 2] = 0; - HEAP32[$1 + 12 >> 2] = 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 1124 >> 2] = 0; - HEAP32[$1 + 608 >> 2] = 1; - $1 = HEAP32[$0 >> 2]; - HEAP32[$1 + 28 >> 2] = 0; - FLAC__ogg_decoder_aspect_set_defaults($1 + 32 | 0); - HEAP32[HEAP32[$0 >> 2] >> 2] = 9; - } - return $3 | 0; - } - - function FLAC__stream_decoder_init_stream($0, $1, $2, $3, $4, $5, $6, $7, $8, $9) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - $7 = $7 | 0; - $8 = $8 | 0; - $9 = $9 | 0; - return init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, 0) | 0; - } - - function init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) { - var $11 = 0, $12 = 0; - $11 = 5; - label$1 : { - $12 = HEAP32[$0 >> 2]; - label$2 : { - if (HEAP32[$12 >> 2] != 9) { - break label$2 - } - $11 = 2; - if (!$8 | (!$1 | !$6)) { - break label$2 - } - if ($2) { - if (!$5 | (!$3 | !$4)) { - break label$2 - } - } - $11 = HEAP32[$0 + 4 >> 2]; - HEAP32[$11 >> 2] = $10; - if ($10) { - if (!FLAC__ogg_decoder_aspect_init($12 + 32 | 0)) { - break label$1 - } - $11 = HEAP32[$0 + 4 >> 2]; - } - FLAC__cpu_info($11 + 3524 | 0); - $10 = HEAP32[$0 + 4 >> 2]; - HEAP32[$10 + 44 >> 2] = 5; - HEAP32[$10 + 40 >> 2] = 6; - HEAP32[$10 + 36 >> 2] = 5; - if (!FLAC__bitreader_init(HEAP32[$10 + 56 >> 2], $0)) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - return 3; - } - $10 = HEAP32[$0 + 4 >> 2]; - HEAP32[$10 + 48 >> 2] = $9; - HEAP32[$10 + 32 >> 2] = $8; - HEAP32[$10 + 28 >> 2] = $7; - HEAP32[$10 + 24 >> 2] = $6; - HEAP32[$10 + 20 >> 2] = $5; - HEAP32[$10 + 16 >> 2] = $4; - HEAP32[$10 + 12 >> 2] = $3; - HEAP32[$10 + 8 >> 2] = $2; - HEAP32[$10 + 4 >> 2] = $1; - HEAP32[$10 + 3520 >> 2] = 0; - HEAP32[$10 + 248 >> 2] = 0; - HEAP32[$10 + 240 >> 2] = 0; - HEAP32[$10 + 244 >> 2] = 0; - HEAP32[$10 + 228 >> 2] = 0; - HEAP32[$10 + 232 >> 2] = 0; - HEAP32[$10 + 3624 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; - HEAP32[$10 + 3628 >> 2] = 1; - HEAP32[$10 + 3632 >> 2] = 0; - $11 = FLAC__stream_decoder_reset($0) ? 0 : 3; - } - return $11; - } - HEAP32[HEAP32[$0 >> 2] + 4 >> 2] = 4; - return 4; - } - - function read_callback_($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0; - label$1 : { - $3 = HEAP32[$2 + 4 >> 2]; - if (HEAP32[$3 >> 2]) { - break label$1 - } - $4 = HEAP32[$3 + 20 >> 2]; - if (!$4) { - break label$1 - } - if (!FUNCTION_TABLE[$4]($2, HEAP32[$3 + 48 >> 2])) { - break label$1 - } - HEAP32[$1 >> 2] = 0; - HEAP32[HEAP32[$2 >> 2] >> 2] = 4; - return 0; - } - label$2 : { - label$3 : { - if (HEAP32[$1 >> 2]) { - $3 = HEAP32[$2 + 4 >> 2]; - if (!(!HEAP32[$3 + 3632 >> 2] | HEAPU32[$3 + 6152 >> 2] < 21)) { - HEAP32[HEAP32[$2 >> 2] >> 2] = 7; - break label$3; - } - label$6 : { - label$7 : { - label$8 : { - label$9 : { - if (HEAP32[$3 >> 2]) { - $4 = 0; - switch (FLAC__ogg_decoder_aspect_read_callback_wrapper(HEAP32[$2 >> 2] + 32 | 0, $0, $1, $2, HEAP32[$3 + 48 >> 2]) | 0) { - case 0: - case 2: - break label$7; - case 1: - break label$8; - default: - break label$9; - }; - } - $4 = FUNCTION_TABLE[HEAP32[$3 + 4 >> 2]]($2, $0, $1, HEAP32[$3 + 48 >> 2]) | 0; - if (($4 | 0) != 2) { - break label$7 - } - } - HEAP32[HEAP32[$2 >> 2] >> 2] = 7; - break label$3; - } - $0 = 1; - if (!HEAP32[$1 >> 2]) { - break label$6 - } - break label$2; - } - $0 = 1; - if (HEAP32[$1 >> 2]) { - break label$2 - } - if (($4 | 0) == 1) { - break label$6 - } - $1 = HEAP32[$2 + 4 >> 2]; - if (HEAP32[$1 >> 2]) { - break label$2 - } - $3 = HEAP32[$1 + 20 >> 2]; - if (!$3) { - break label$2 - } - if (!FUNCTION_TABLE[$3]($2, HEAP32[$1 + 48 >> 2])) { - break label$2 - } - } - HEAP32[HEAP32[$2 >> 2] >> 2] = 4; - break label$3; - } - HEAP32[HEAP32[$2 >> 2] >> 2] = 7; - } - $0 = 0; - } - return $0 | 0; - } - - function FLAC__stream_decoder_reset($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0; - $1 = HEAP32[$0 + 4 >> 2]; - label$1 : { - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9 ? !HEAP32[$1 + 3628 >> 2] : 0) { - break label$1 - } - HEAP32[$1 + 3624 >> 2] = 0; - HEAP32[$1 + 240 >> 2] = 0; - HEAP32[$1 + 244 >> 2] = 0; - if (HEAP32[$1 >> 2]) { - $1 = HEAP32[$0 >> 2] + 32 | 0; - ogg_stream_reset($1 + 8 | 0); - ogg_sync_reset($1 + 368 | 0); - HEAP32[$1 + 408 >> 2] = 0; - HEAP32[$1 + 412 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - } - $1 = HEAP32[$1 + 56 >> 2]; - HEAP32[$1 + 8 >> 2] = 0; - HEAP32[$1 + 12 >> 2] = 0; - HEAP32[$1 + 16 >> 2] = 0; - HEAP32[$1 + 20 >> 2] = 0; - $1 = 1; - $2 = HEAP32[$0 >> 2]; - if (!$1) { - HEAP32[$2 >> 2] = 8; - return 0; - } - HEAP32[$2 >> 2] = 2; - $1 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$1 >> 2]) { - FLAC__ogg_decoder_aspect_reset($2 + 32 | 0); - $1 = HEAP32[$0 + 4 >> 2]; - } - label$6 : { - if (!HEAP32[$1 + 3628 >> 2]) { - $2 = 0; - if (HEAP32[$1 + 52 >> 2] == HEAP32[1887]) { - break label$1 - } - $3 = HEAP32[$1 + 8 >> 2]; - if (!$3) { - break label$6 - } - if ((FUNCTION_TABLE[$3]($0, 0, 0, HEAP32[$1 + 48 >> 2]) | 0) == 1) { - break label$1 - } - $1 = HEAP32[$0 + 4 >> 2]; - break label$6; - } - HEAP32[$1 + 3628 >> 2] = 0; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 0; - HEAP32[$1 + 248 >> 2] = 0; - dlfree(HEAP32[$1 + 452 >> 2]); - HEAP32[HEAP32[$0 + 4 >> 2] + 452 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 252 >> 2] = 0; - HEAP32[$1 + 3624 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; - HEAP32[$1 + 228 >> 2] = 0; - HEAP32[$1 + 232 >> 2] = 0; - FLAC__MD5Init($1 + 3636 | 0); - $0 = HEAP32[$0 + 4 >> 2]; - HEAP32[$0 + 6152 >> 2] = 0; - HEAP32[$0 + 6136 >> 2] = 0; - HEAP32[$0 + 6140 >> 2] = 0; - $2 = 1; - } - return $2 | 0; - } - - function FLAC__stream_decoder_init_ogg_stream($0, $1, $2, $3, $4, $5, $6, $7, $8, $9) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - $7 = $7 | 0; - $8 = $8 | 0; - $9 = $9 | 0; - return init_stream_internal_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, 1) | 0; - } - - function FLAC__stream_decoder_set_ogg_serial_number($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 9) { - $0 = $0 + 32 | 0; - HEAP32[$0 + 4 >> 2] = $1; - HEAP32[$0 >> 2] = 0; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_decoder_set_md5_checking($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 9) { - HEAP32[$0 + 28 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_decoder_set_metadata_respond($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0; - label$1 : { - if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9 | $1 >>> 0 > 126) { - break label$1 - } - $2 = 1; - $0 = HEAP32[$0 + 4 >> 2]; - HEAP32[($0 + ($1 << 2) | 0) + 608 >> 2] = 1; - if (($1 | 0) != 2) { - break label$1 - } - HEAP32[$0 + 1124 >> 2] = 0; - } - return $2 | 0; - } - - function FLAC__stream_decoder_set_metadata_respond_application($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0, $3 = 0, $4 = 0; - $2 = 0; - label$1 : { - if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9) { - break label$1 - } - $3 = HEAP32[$0 + 4 >> 2]; - $2 = 1; - if (HEAP32[$3 + 616 >> 2]) { - break label$1 - } - $2 = HEAP32[$3 + 1120 >> 2]; - label$2 : { - $4 = HEAP32[$3 + 1124 >> 2]; - label$3 : { - if (($4 | 0) != HEAP32[$3 + 1128 >> 2]) { - $3 = $2; - break label$3; - } - label$5 : { - if (!$4) { - $3 = dlrealloc($2, 0); - break label$5; - } - if ($4 + $4 >>> 0 >= $4 >>> 0) { - $3 = dlrealloc($2, $4 << 1); - if ($3) { - break label$5 - } - dlfree($2); - $3 = HEAP32[$0 + 4 >> 2]; - } - HEAP32[$3 + 1120 >> 2] = 0; - break label$2; - } - $2 = HEAP32[$0 + 4 >> 2]; - HEAP32[$2 + 1120 >> 2] = $3; - if (!$3) { - break label$2 - } - HEAP32[$2 + 1128 >> 2] = HEAP32[$2 + 1128 >> 2] << 1; - $4 = HEAP32[$2 + 1124 >> 2]; - } - $2 = $3; - $3 = HEAP32[1364] >>> 3 | 0; - memcpy($2 + Math_imul($3, $4) | 0, $1, $3); - $0 = HEAP32[$0 + 4 >> 2]; - HEAP32[$0 + 1124 >> 2] = HEAP32[$0 + 1124 >> 2] + 1; - return 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $2 = 0; - } - return $2 | 0; - } - - function FLAC__stream_decoder_set_metadata_respond_all($0) { - $0 = $0 | 0; - var $1 = 0; - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { - $1 = HEAP32[$0 + 4 >> 2]; - $0 = 0; - while (1) { - HEAP32[($1 + ($0 << 2) | 0) + 608 >> 2] = 1; - $0 = $0 + 1 | 0; - if (($0 | 0) != 128) { - continue - } - break; - }; - HEAP32[$1 + 1124 >> 2] = 0; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_decoder_set_metadata_ignore($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0; - label$1 : { - if (HEAP32[HEAP32[$0 >> 2] >> 2] != 9 | $1 >>> 0 > 126) { - break label$1 - } - $0 = HEAP32[$0 + 4 >> 2]; - HEAP32[($0 + ($1 << 2) | 0) + 608 >> 2] = 0; - $2 = 1; - if (($1 | 0) != 2) { - break label$1 - } - HEAP32[$0 + 1124 >> 2] = 0; - } - return $2 | 0; - } - - function FLAC__stream_decoder_set_metadata_ignore_application($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0, $3 = 0, $4 = 0; - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { - $2 = HEAP32[$0 + 4 >> 2]; - if (!HEAP32[$2 + 616 >> 2]) { - return 1 - } - $3 = HEAP32[$2 + 1120 >> 2]; - label$3 : { - $4 = HEAP32[$2 + 1124 >> 2]; - label$4 : { - if (($4 | 0) != HEAP32[$2 + 1128 >> 2]) { - $2 = $3; - break label$4; - } - label$6 : { - if (!$4) { - $2 = dlrealloc($3, 0); - break label$6; - } - if ($4 + $4 >>> 0 >= $4 >>> 0) { - $2 = dlrealloc($3, $4 << 1); - if ($2) { - break label$6 - } - dlfree($3); - $2 = HEAP32[$0 + 4 >> 2]; - } - HEAP32[$2 + 1120 >> 2] = 0; - break label$3; - } - $3 = HEAP32[$0 + 4 >> 2]; - HEAP32[$3 + 1120 >> 2] = $2; - if (!$2) { - break label$3 - } - HEAP32[$3 + 1128 >> 2] = HEAP32[$3 + 1128 >> 2] << 1; - $4 = HEAP32[$3 + 1124 >> 2]; - } - $3 = $2; - $2 = HEAP32[1364] >>> 3 | 0; - memcpy($3 + Math_imul($2, $4) | 0, $1, $2); - $0 = HEAP32[$0 + 4 >> 2]; - HEAP32[$0 + 1124 >> 2] = HEAP32[$0 + 1124 >> 2] + 1; - return 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - } - return 0; - } - - function FLAC__stream_decoder_set_metadata_ignore_all($0) { - $0 = $0 | 0; - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 9) { - memset(HEAP32[$0 + 4 >> 2] + 608 | 0, 512); - HEAP32[HEAP32[$0 + 4 >> 2] + 1124 >> 2] = 0; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_decoder_get_state($0) { - $0 = $0 | 0; - return HEAP32[HEAP32[$0 >> 2] >> 2]; - } - - function FLAC__stream_decoder_get_md5_checking($0) { - $0 = $0 | 0; - return HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; - } - - function FLAC__stream_decoder_process_single($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0; - $1 = global$0 - 16 | 0; - global$0 = $1; - $2 = 1; - label$1 : { - while (1) { - label$3 : { - label$4 : { - switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { - case 0: - if (find_metadata_($0)) { - continue - } - $2 = 0; - break label$3; - case 1: - $3 = (read_metadata_($0) | 0) != 0; - break label$1; - case 2: - if (frame_sync_($0)) { - continue - } - break label$3; - case 4: - case 7: - break label$3; - case 3: - break label$4; - default: - break label$1; - }; - } - if (!read_frame_($0, $1 + 12 | 0)) { - $2 = 0; - break label$3; - } - if (!HEAP32[$1 + 12 >> 2]) { - continue - } - } - break; - }; - $3 = $2; - } - global$0 = $1 + 16 | 0; - return $3 | 0; - } - - function find_metadata_($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - $5 = 1; - label$1 : { - while (1) { - $1 = 0; - label$3 : { - while (1) { - $6 = HEAP32[$0 + 4 >> 2]; - label$5 : { - if (HEAP32[$6 + 3520 >> 2]) { - $4 = HEAPU8[$6 + 3590 | 0]; - HEAP32[$2 + 8 >> 2] = $4; - HEAP32[$6 + 3520 >> 2] = 0; - break label$5; - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$6 + 56 >> 2], $2 + 8 | 0, 8)) { - $3 = 0; - break label$1; - } - $4 = HEAP32[$2 + 8 >> 2]; - } - if (HEAPU8[$3 + 5409 | 0] == ($4 | 0)) { - $3 = $3 + 1 | 0; - $1 = 1; - break label$3; - } - $3 = 0; - if (($1 | 0) == 3) { - break label$1 - } - if (HEAPU8[$1 + 7552 | 0] == ($4 | 0)) { - $1 = $1 + 1 | 0; - if (($1 | 0) != 3) { - continue - } - label$10 : { - label$11 : { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 24)) { - break label$11 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { - break label$11 - } - $4 = HEAP32[$2 + 12 >> 2]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { - break label$11 - } - $6 = HEAP32[$2 + 12 >> 2]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { - break label$11 - } - $7 = HEAP32[$2 + 12 >> 2]; - if (FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 12 | 0, 8)) { - break label$10 - } - } - break label$1; - } - if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], HEAP32[$2 + 12 >> 2] & 127 | ($7 << 7 & 16256 | ($6 & 127 | $4 << 7 & 16256) << 14))) { - continue - } - break label$1; - } - break; - }; - label$12 : { - if (($4 | 0) != 255) { - break label$12 - } - HEAP8[HEAP32[$0 + 4 >> 2] + 3588 | 0] = 255; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $2 + 8 | 0, 8)) { - break label$1 - } - $1 = HEAP32[$2 + 8 >> 2]; - if (($1 | 0) == 255) { - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 3520 >> 2] = 1; - HEAP8[$1 + 3590 | 0] = 255; - break label$12; - } - if (($1 & -2) != 248) { - break label$12 - } - HEAP8[HEAP32[$0 + 4 >> 2] + 3589 | 0] = $1; - HEAP32[HEAP32[$0 >> 2] >> 2] = 3; - $3 = 1; - break label$1; - } - $1 = 0; - if (!$5) { - break label$3 - } - $5 = HEAP32[$0 + 4 >> 2]; - $1 = 0; - if (HEAP32[$5 + 3632 >> 2]) { - break label$3 - } - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 0, HEAP32[$5 + 48 >> 2]); - $1 = 0; - } - $5 = $1; - if ($3 >>> 0 < 4) { - continue - } - break; - }; - $3 = 1; - HEAP32[HEAP32[$0 >> 2] >> 2] = 1; - } - global$0 = $2 + 16 | 0; - return $3; - } - - function read_metadata_($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0; - $7 = global$0 - 192 | 0; - global$0 = $7; - label$1 : { - label$2 : { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $7 + 184 | 0, HEAP32[1391])) { - break label$2 - } - $15 = HEAP32[$7 + 184 >> 2]; - $4 = $0 + 4 | 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 180 | 0, HEAP32[1392])) { - break label$1 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 176 | 0, HEAP32[1393])) { - break label$1 - } - $6 = ($15 | 0) != 0; - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - $2 = HEAP32[$7 + 180 >> 2]; - switch ($2 | 0) { - case 3: - break label$6; - case 0: - break label$7; - default: - break label$5; - }; - } - $3 = HEAP32[$7 + 176 >> 2]; - $2 = 0; - $1 = HEAP32[$4 >> 2]; - HEAP32[$1 + 256 >> 2] = 0; - HEAP32[$1 + 264 >> 2] = $3; - HEAP32[$1 + 260 >> 2] = $6; - $5 = HEAP32[$1 + 56 >> 2]; - $1 = HEAP32[1356]; - if (!FLAC__bitreader_read_raw_uint32($5, $7, $1)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 272 >> 2] = HEAP32[$7 >> 2]; - $5 = HEAP32[1357]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $5)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 276 >> 2] = HEAP32[$7 >> 2]; - $6 = HEAP32[1358]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $6)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 280 >> 2] = HEAP32[$7 >> 2]; - $8 = HEAP32[1359]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $8)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 284 >> 2] = HEAP32[$7 >> 2]; - $9 = HEAP32[1360]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $9)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 288 >> 2] = HEAP32[$7 >> 2]; - $10 = HEAP32[1361]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $10)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 292 >> 2] = HEAP32[$7 >> 2] + 1; - $11 = HEAP32[1362]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7, $11)) { - break label$1 - } - HEAP32[HEAP32[$4 >> 2] + 296 >> 2] = HEAP32[$7 >> 2] + 1; - $12 = HEAP32[$4 >> 2]; - $13 = HEAP32[$12 + 56 >> 2]; - $14 = $12 + 304 | 0; - $12 = HEAP32[1363]; - if (!FLAC__bitreader_read_raw_uint64($13, $14, $12)) { - break label$1 - } - $13 = HEAP32[$4 >> 2]; - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$13 + 56 >> 2], $13 + 312 | 0, 16)) { - break label$1 - } - if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3 - (($12 + ($11 + ($10 + ($9 + ($8 + ($6 + ($1 + $5 | 0) | 0) | 0) | 0) | 0) | 0) | 0) + 128 >>> 3 | 0) | 0)) { - break label$2 - } - $1 = HEAP32[$4 >> 2]; - HEAP32[$1 + 248 >> 2] = 1; - if (!memcmp($1 + 312 | 0, 7555, 16)) { - HEAP32[$1 + 3624 >> 2] = 0 - } - if (HEAP32[$1 + 3632 >> 2] | !HEAP32[$1 + 608 >> 2]) { - break label$4 - } - $2 = HEAP32[$1 + 28 >> 2]; - if (!$2) { - break label$4 - } - FUNCTION_TABLE[$2]($0, $1 + 256 | 0, HEAP32[$1 + 48 >> 2]); - break label$4; - } - $1 = HEAP32[$4 >> 2]; - HEAP32[$1 + 252 >> 2] = 0; - $5 = HEAP32[$7 + 176 >> 2]; - HEAP32[$1 + 448 >> 2] = ($5 >>> 0) / 18; - HEAP32[$1 + 440 >> 2] = $5; - HEAP32[$1 + 436 >> 2] = $6; - HEAP32[$1 + 432 >> 2] = 3; - $1 = HEAP32[$4 >> 2]; - $2 = HEAP32[$1 + 452 >> 2]; - $3 = HEAP32[$1 + 448 >> 2]; - label$9 : { - if ($3) { - __wasm_i64_mul($3, 0, 24, 0); - if (!i64toi32_i32$HIGH_BITS) { - $1 = dlrealloc($2, Math_imul($3, 24)); - if ($1) { - HEAP32[HEAP32[$4 >> 2] + 452 >> 2] = $1; - break label$9; - } - dlfree($2); - $1 = HEAP32[$4 >> 2]; - } - HEAP32[$1 + 452 >> 2] = 0; - break label$3; - } - $1 = dlrealloc($2, 0); - HEAP32[HEAP32[$4 >> 2] + 452 >> 2] = $1; - if (!$1) { - break label$3 - } - } - $2 = HEAP32[$4 >> 2]; - $1 = 0; - label$14 : { - if (!HEAP32[$2 + 448 >> 2]) { - break label$14 - } - $6 = HEAP32[1367]; - $8 = HEAP32[1366]; - $9 = HEAP32[1365]; - $3 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_uint64(HEAP32[$2 + 56 >> 2], $7, $9)) { - break label$2 - } - $2 = HEAP32[$7 + 4 >> 2]; - $1 = Math_imul($3, 24); - $10 = HEAP32[$4 >> 2]; - $11 = $1 + HEAP32[$10 + 452 >> 2] | 0; - HEAP32[$11 >> 2] = HEAP32[$7 >> 2]; - HEAP32[$11 + 4 >> 2] = $2; - if (!FLAC__bitreader_read_raw_uint64(HEAP32[$10 + 56 >> 2], $7, $8)) { - break label$2 - } - $2 = HEAP32[$7 + 4 >> 2]; - $10 = HEAP32[$4 >> 2]; - $11 = $1 + HEAP32[$10 + 452 >> 2] | 0; - HEAP32[$11 + 8 >> 2] = HEAP32[$7 >> 2]; - HEAP32[$11 + 12 >> 2] = $2; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$10 + 56 >> 2], $7 + 188 | 0, $6)) { - break label$2 - } - $2 = HEAP32[$4 >> 2]; - HEAP32[($1 + HEAP32[$2 + 452 >> 2] | 0) + 16 >> 2] = HEAP32[$7 + 188 >> 2]; - $3 = $3 + 1 | 0; - $1 = HEAP32[$2 + 448 >> 2]; - if ($3 >>> 0 < $1 >>> 0) { - continue - } - break; - }; - $1 = Math_imul($1, -18); - } - $1 = $1 + $5 | 0; - if ($1) { - if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$2 + 56 >> 2], $1)) { - break label$2 - } - $2 = HEAP32[$4 >> 2]; - } - HEAP32[$2 + 252 >> 2] = 1; - if (HEAP32[$2 + 3632 >> 2] | !HEAP32[$2 + 620 >> 2]) { - break label$4 - } - $1 = HEAP32[$2 + 28 >> 2]; - if (!$1) { - break label$4 - } - FUNCTION_TABLE[$1]($0, $2 + 432 | 0, HEAP32[$2 + 48 >> 2]); - break label$4; - } - $3 = HEAP32[$4 >> 2]; - $8 = HEAP32[($3 + ($2 << 2) | 0) + 608 >> 2]; - $5 = HEAP32[$7 + 176 >> 2]; - $1 = memset($7, 176); - HEAP32[$1 + 8 >> 2] = $5; - HEAP32[$1 >> 2] = $2; - HEAP32[$1 + 4 >> 2] = $6; - $9 = !$8; - label$17 : { - if (($2 | 0) != 2) { - break label$17 - } - $10 = $1 + 16 | 0; - $6 = HEAP32[1364] >>> 3 | 0; - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $10, $6)) { - break label$2 - } - if ($5 >>> 0 < $6 >>> 0) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $2 = 0; - break label$1; - } - $5 = $5 - $6 | 0; - $3 = HEAP32[$4 >> 2]; - $11 = HEAP32[$3 + 1124 >> 2]; - if (!$11) { - break label$17 - } - $12 = HEAP32[$3 + 1120 >> 2]; - $2 = 0; - while (1) { - if (memcmp($12 + Math_imul($2, $6) | 0, $10, $6)) { - $2 = $2 + 1 | 0; - if (($11 | 0) != ($2 | 0)) { - continue - } - break label$17; - } - break; - }; - $9 = ($8 | 0) != 0; - } - if ($9) { - if (!FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $5)) { - break label$2 - } - break label$4; - } - label$22 : { - label$23 : { - label$24 : { - label$25 : { - label$26 : { - label$27 : { - label$28 : { - switch (HEAP32[$1 + 180 >> 2]) { - case 1: - if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $5)) { - break label$26 - } - $6 = 0; - break label$22; - case 2: - if (!$5) { - break label$27 - } - $2 = dlmalloc($5); - HEAP32[$1 + 20 >> 2] = $2; - if (!$2) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $6 = 0; - break label$22; - } - if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, $5)) { - break label$26 - } - $6 = 0; - break label$22; - case 4: - label$35 : { - if ($5 >>> 0 < 8) { - break label$35 - } - $6 = 0; - if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[$3 + 56 >> 2], $1 + 16 | 0)) { - break label$22 - } - $5 = $5 + -8 | 0; - $2 = HEAP32[$1 + 16 >> 2]; - label$36 : { - if ($2) { - if ($5 >>> 0 < $2 >>> 0) { - HEAP32[$1 + 16 >> 2] = 0; - HEAP32[$1 + 20 >> 2] = 0; - break label$35; - } - label$39 : { - label$40 : { - if (($2 | 0) == -1) { - HEAP32[$1 + 20 >> 2] = 0; - break label$40; - } - $3 = dlmalloc($2 + 1 | 0); - HEAP32[$1 + 20 >> 2] = $3; - if ($3) { - break label$39 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$22; - } - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { - break label$22 - } - $5 = $5 - $2 | 0; - HEAP8[HEAP32[$1 + 20 >> 2] + HEAP32[$1 + 16 >> 2] | 0] = 0; - break label$36; - } - HEAP32[$1 + 20 >> 2] = 0; - } - if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 24 | 0)) { - break label$22 - } - $2 = HEAP32[$1 + 24 >> 2]; - if ($2 >>> 0 >= 100001) { - HEAP32[$1 + 24 >> 2] = 0; - break label$22; - } - if (!$2) { - break label$35 - } - $3 = safe_malloc_mul_2op_p($2, 8); - HEAP32[$1 + 28 >> 2] = $3; - if (!$3) { - break label$24 - } - if (!HEAP32[$1 + 24 >> 2]) { - break label$35 - } - HEAP32[$3 >> 2] = 0; - HEAP32[$3 + 4 >> 2] = 0; - $2 = 0; - label$43 : { - if ($5 >>> 0 < 4) { - break label$43 - } - while (1) { - if (!FLAC__bitreader_read_uint32_little_endian(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3)) { - break label$23 - } - $5 = $5 + -4 | 0; - $8 = HEAP32[$1 + 28 >> 2]; - $9 = $2 << 3; - $3 = $8 + $9 | 0; - $6 = HEAP32[$3 >> 2]; - label$45 : { - if ($6) { - if ($5 >>> 0 < $6 >>> 0) { - break label$43 - } - label$47 : { - label$48 : { - if (($6 | 0) == -1) { - HEAP32[($8 + ($2 << 3) | 0) + 4 >> 2] = 0; - break label$48; - } - $8 = dlmalloc($6 + 1 | 0); - HEAP32[$3 + 4 >> 2] = $8; - if ($8) { - break label$47 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$23; - } - $5 = $5 - $6 | 0; - memset($8, HEAP32[$3 >> 2]); - $6 = FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], HEAP32[$3 + 4 >> 2], HEAP32[$3 >> 2]); - $8 = $9 + HEAP32[$1 + 28 >> 2] | 0; - $3 = HEAP32[$8 + 4 >> 2]; - if (!$6) { - dlfree($3); - HEAP32[(HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0) + 4 >> 2] = 0; - break label$43; - } - HEAP8[$3 + HEAP32[$8 >> 2] | 0] = 0; - break label$45; - } - HEAP32[$3 + 4 >> 2] = 0; - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 >= HEAPU32[$1 + 24 >> 2]) { - break label$35 - } - $3 = HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0; - HEAP32[$3 >> 2] = 0; - HEAP32[$3 + 4 >> 2] = 0; - if ($5 >>> 0 >= 4) { - continue - } - break; - }; - } - HEAP32[$1 + 24 >> 2] = $2; - } - if (!$5) { - break label$26 - } - if (!HEAP32[$1 + 24 >> 2]) { - $2 = $1 + 28 | 0; - dlfree(HEAP32[$2 >> 2]); - HEAP32[$2 >> 2] = 0; - } - if (FLAC__bitreader_skip_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $5)) { - break label$26 - } - $6 = 0; - break label$22; - case 5: - $6 = 0; - $2 = memset($1 + 16 | 0, 160); - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, HEAP32[1378] >>> 3 | 0)) { - break label$22 - } - if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 152 | 0, HEAP32[1379])) { - break label$22 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1380])) { - break label$22 - } - HEAP32[$1 + 160 >> 2] = HEAP32[$1 + 188 >> 2] != 0; - if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], HEAP32[1381])) { - break label$22 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1382])) { - break label$22 - } - $2 = HEAP32[$1 + 188 >> 2]; - HEAP32[$1 + 164 >> 2] = $2; - if (!$2) { - break label$26 - } - $2 = dlcalloc($2, 32); - HEAP32[$1 + 168 >> 2] = $2; - if (!$2) { - break label$25 - } - $9 = HEAP32[1371]; - if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2, $9)) { - break label$22 - } - $10 = HEAP32[1373] >>> 3 | 0; - $11 = HEAP32[1370]; - $12 = HEAP32[1369]; - $8 = HEAP32[1368]; - $13 = HEAP32[1377]; - $16 = HEAP32[1376]; - $17 = HEAP32[1375]; - $18 = HEAP32[1374]; - $19 = HEAP32[1372]; - $5 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $19)) { - break label$22 - } - $2 = ($5 << 5) + $2 | 0; - HEAP8[$2 + 8 | 0] = HEAP32[$1 + 188 >> 2]; - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2 + 9 | 0, $10)) { - break label$22 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $18)) { - break label$22 - } - HEAP8[$2 + 22 | 0] = HEAPU8[$2 + 22 | 0] & 254 | HEAP8[$1 + 188 | 0] & 1; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $17)) { - break label$22 - } - $3 = $2 + 22 | 0; - HEAP8[$3 | 0] = HEAPU8[$1 + 188 | 0] << 1 & 2 | HEAPU8[$3 | 0] & 253; - if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $16)) { - break label$22 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $13)) { - break label$22 - } - $3 = HEAP32[$1 + 188 >> 2]; - HEAP8[$2 + 23 | 0] = $3; - label$53 : { - $3 = $3 & 255; - if (!$3) { - break label$53 - } - $3 = dlcalloc($3, 16); - HEAP32[$2 + 24 >> 2] = $3; - label$54 : { - if ($3) { - $14 = $2 + 23 | 0; - if (!HEAPU8[$14 | 0]) { - break label$53 - } - if (!FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $8)) { - break label$22 - } - $20 = $2 + 24 | 0; - $2 = 0; - break label$54; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$22; - } - while (1) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, $12)) { - break label$22 - } - HEAP8[(($2 << 4) + $3 | 0) + 8 | 0] = HEAP32[$1 + 188 >> 2]; - if (!FLAC__bitreader_skip_bits_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $11)) { - break label$22 - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 >= HEAPU8[$14 | 0]) { - break label$53 - } - $3 = HEAP32[$20 >> 2]; - if (FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3 + ($2 << 4) | 0, $8)) { - continue - } - break; - }; - break label$22; - } - $5 = $5 + 1 | 0; - if ($5 >>> 0 >= HEAPU32[$1 + 164 >> 2]) { - break label$26 - } - $2 = HEAP32[$1 + 168 >> 2]; - if (FLAC__bitreader_read_raw_uint64(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $2 + ($5 << 5) | 0, $9)) { - continue - } - break; - }; - break label$22; - case 6: - label$57 : { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$3 + 56 >> 2], $1 + 188 | 0, HEAP32[1383])) { - break label$57 - } - HEAP32[$1 + 16 >> 2] = HEAP32[$1 + 188 >> 2]; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1384])) { - break label$57 - } - label$58 : { - $2 = HEAP32[$1 + 188 >> 2]; - label$59 : { - if (($2 | 0) == -1) { - HEAP32[$1 + 20 >> 2] = 0; - break label$59; - } - $3 = dlmalloc($2 + 1 | 0); - HEAP32[$1 + 20 >> 2] = $3; - if ($3) { - break label$58 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $6 = 0; - break label$22; - } - if ($2) { - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { - break label$57 - } - $3 = HEAP32[$1 + 20 >> 2]; - $2 = HEAP32[$1 + 188 >> 2]; - } else { - $2 = 0 - } - HEAP8[$2 + $3 | 0] = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 188 | 0, HEAP32[1385])) { - break label$57 - } - label$63 : { - $2 = HEAP32[$1 + 188 >> 2]; - label$64 : { - if (($2 | 0) == -1) { - HEAP32[$1 + 24 >> 2] = 0; - break label$64; - } - $3 = dlmalloc($2 + 1 | 0); - HEAP32[$1 + 24 >> 2] = $3; - if ($3) { - break label$63 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $6 = 0; - break label$22; - } - if ($2) { - if (!FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { - break label$57 - } - $3 = HEAP32[$1 + 24 >> 2]; - $2 = HEAP32[$1 + 188 >> 2]; - } else { - $2 = 0 - } - HEAP8[$2 + $3 | 0] = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 28 | 0, HEAP32[1386])) { - break label$57 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 32 | 0, HEAP32[1387])) { - break label$57 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 36 | 0, HEAP32[1388])) { - break label$57 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 40 | 0, HEAP32[1389])) { - break label$57 - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $1 + 44 | 0, HEAP32[1390])) { - break label$57 - } - $2 = HEAP32[$1 + 44 >> 2]; - $3 = dlmalloc($2 ? $2 : 1); - HEAP32[$1 + 48 >> 2] = $3; - if (!$3) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $6 = 0; - break label$22; - } - if (!$2) { - break label$26 - } - if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $3, $2)) { - break label$26 - } - } - $6 = 0; - break label$22; - case 0: - case 3: - break label$26; - default: - break label$28; - }; - } - label$69 : { - if ($5) { - $2 = dlmalloc($5); - HEAP32[$1 + 16 >> 2] = $2; - if ($2) { - break label$69 - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $6 = 0; - break label$22; - } - HEAP32[$1 + 16 >> 2] = 0; - break label$26; - } - if (FLAC__bitreader_read_byte_block_aligned_no_crc(HEAP32[$3 + 56 >> 2], $2, $5)) { - break label$26 - } - $6 = 0; - break label$22; - } - HEAP32[$1 + 20 >> 2] = 0; - } - $6 = 1; - $2 = HEAP32[$4 >> 2]; - if (HEAP32[$2 + 3632 >> 2]) { - break label$22 - } - $3 = HEAP32[$2 + 28 >> 2]; - if (!$3) { - break label$22 - } - FUNCTION_TABLE[$3]($0, $1, HEAP32[$2 + 48 >> 2]); - break label$22; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$22; - } - HEAP32[$1 + 24 >> 2] = 0; - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$22; - } - HEAP32[$1 + 24 >> 2] = $2; - $6 = 0; - } - label$71 : { - label$72 : { - switch (HEAP32[$1 + 180 >> 2] + -1 | 0) { - case 1: - $1 = HEAP32[$1 + 20 >> 2]; - if (!$1) { - break label$71 - } - dlfree($1); - break label$71; - case 3: - $2 = HEAP32[$1 + 20 >> 2]; - if ($2) { - dlfree($2) - } - $3 = HEAP32[$1 + 24 >> 2]; - if ($3) { - $2 = 0; - while (1) { - $5 = HEAP32[(HEAP32[$1 + 28 >> 2] + ($2 << 3) | 0) + 4 >> 2]; - if ($5) { - dlfree($5); - $3 = HEAP32[$1 + 24 >> 2]; - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 < $3 >>> 0) { - continue - } - break; - }; - } - $1 = HEAP32[$1 + 28 >> 2]; - if (!$1) { - break label$71 - } - dlfree($1); - break label$71; - case 4: - $3 = HEAP32[$1 + 164 >> 2]; - if ($3) { - $2 = 0; - while (1) { - $5 = HEAP32[(HEAP32[$1 + 168 >> 2] + ($2 << 5) | 0) + 24 >> 2]; - if ($5) { - dlfree($5); - $3 = HEAP32[$1 + 164 >> 2]; - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 < $3 >>> 0) { - continue - } - break; - }; - } - $1 = HEAP32[$1 + 168 >> 2]; - if (!$1) { - break label$71 - } - dlfree($1); - break label$71; - case 5: - $2 = HEAP32[$1 + 20 >> 2]; - if ($2) { - dlfree($2) - } - $2 = HEAP32[$1 + 24 >> 2]; - if ($2) { - dlfree($2) - } - $1 = HEAP32[$1 + 48 >> 2]; - if (!$1) { - break label$71 - } - dlfree($1); - break label$71; - case 0: - break label$71; - default: - break label$72; - }; - } - $1 = HEAP32[$1 + 16 >> 2]; - if (!$1) { - break label$71 - } - dlfree($1); - } - if (!$6) { - break label$2 - } - } - $2 = 1; - if (!$15) { - break label$1 - } - label$86 : { - label$87 : { - $3 = HEAP32[$4 >> 2]; - if (HEAP32[$3 >> 2]) { - break label$87 - } - $5 = HEAP32[$3 + 12 >> 2]; - if (!$5) { - break label$87 - } - $1 = $3 + 6136 | 0; - if (FUNCTION_TABLE[$5]($0, $1, HEAP32[$3 + 48 >> 2])) { - break label$87 - } - if (!FLAC__bitreader_is_consumed_byte_aligned(HEAP32[HEAP32[$4 >> 2] + 56 >> 2])) { - break label$87 - } - $3 = HEAP32[$1 >> 2]; - $4 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - $4 = ((HEAP32[$4 + 8 >> 2] - HEAP32[$4 + 16 >> 2] << 5) + (HEAP32[$4 + 12 >> 2] << 3) | 0) - HEAP32[$4 + 20 >> 2] >>> 3 | 0; - $5 = HEAP32[$1 + 4 >> 2] - ($3 >>> 0 < $4 >>> 0) | 0; - HEAP32[$1 >> 2] = $3 - $4; - HEAP32[$1 + 4 >> 2] = $5; - break label$86; - } - $1 = HEAP32[$4 >> 2]; - HEAP32[$1 + 6136 >> 2] = 0; - HEAP32[$1 + 6140 >> 2] = 0; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - } - $2 = 0; - } - global$0 = $7 + 192 | 0; - return $2; - } - - function frame_sync_($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0; - $4 = global$0 - 16 | 0; - global$0 = $4; - label$1 : { - label$2 : { - label$3 : { - $2 = HEAP32[$0 + 4 >> 2]; - if (!HEAP32[$2 + 248 >> 2]) { - break label$3 - } - $3 = HEAP32[$2 + 308 >> 2]; - $1 = $3; - $5 = HEAP32[$2 + 304 >> 2]; - if (!($1 | $5)) { - break label$3 - } - $3 = HEAP32[$2 + 244 >> 2]; - if (($1 | 0) == ($3 | 0) & HEAPU32[$2 + 240 >> 2] < $5 >>> 0 | $3 >>> 0 < $1 >>> 0) { - break label$3 - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 4; - break label$2; - } - label$4 : { - if (FLAC__bitreader_is_consumed_byte_aligned(HEAP32[$2 + 56 >> 2])) { - break label$4 - } - $2 = HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2]; - if (FLAC__bitreader_read_raw_uint32($2, $4 + 12 | 0, FLAC__bitreader_bits_left_for_byte_alignment($2))) { - break label$4 - } - $1 = 0; - break label$1; - } - $2 = 0; - while (1) { - $3 = HEAP32[$0 + 4 >> 2]; - label$6 : { - if (HEAP32[$3 + 3520 >> 2]) { - $1 = HEAPU8[$3 + 3590 | 0]; - HEAP32[$4 + 12 >> 2] = $1; - HEAP32[$3 + 3520 >> 2] = 0; - break label$6; - } - $1 = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$3 + 56 >> 2], $4 + 12 | 0, 8)) { - break label$1 - } - $1 = HEAP32[$4 + 12 >> 2]; - } - label$8 : { - if (($1 | 0) != 255) { - break label$8 - } - HEAP8[HEAP32[$0 + 4 >> 2] + 3588 | 0] = 255; - $1 = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $4 + 12 | 0, 8)) { - break label$1 - } - $1 = HEAP32[$4 + 12 >> 2]; - if (($1 | 0) == 255) { - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 3520 >> 2] = 1; - HEAP8[$1 + 3590 | 0] = 255; - break label$8; - } - if (($1 & -2) != 248) { - break label$8 - } - HEAP8[HEAP32[$0 + 4 >> 2] + 3589 | 0] = $1; - HEAP32[HEAP32[$0 >> 2] >> 2] = 3; - break label$2; - } - $1 = $2; - $2 = 1; - if ($1) { - continue - } - $1 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$1 + 3632 >> 2]) { - continue - } - FUNCTION_TABLE[HEAP32[$1 + 32 >> 2]]($0, 0, HEAP32[$1 + 48 >> 2]); - continue; - }; - } - $1 = 1; - } - global$0 = $4 + 16 | 0; - return $1; - } - - function read_frame_($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $7 = global$0 + -64 | 0; - global$0 = $7; - HEAP32[$1 >> 2] = 0; - $2 = HEAP32[$0 + 4 >> 2]; - $4 = HEAPU16[(HEAPU8[$2 + 3588 | 0] << 1) + 1280 >> 1]; - $5 = HEAP32[$2 + 56 >> 2]; - HEAP32[$5 + 24 >> 2] = HEAPU16[((HEAPU8[$2 + 3589 | 0] ^ $4 >>> 8) << 1) + 1280 >> 1] ^ $4 << 8 & 65280; - $2 = HEAP32[$5 + 20 >> 2]; - HEAP32[$5 + 28 >> 2] = HEAP32[$5 + 16 >> 2]; - HEAP32[$5 + 32 >> 2] = $2; - $5 = HEAP32[$0 + 4 >> 2]; - HEAP8[$7 + 32 | 0] = HEAPU8[$5 + 3588 | 0]; - $2 = HEAPU8[$5 + 3589 | 0]; - HEAP32[$7 + 12 >> 2] = 2; - HEAP8[$7 + 33 | 0] = $2; - label$1 : { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$5 + 56 >> 2], $7 + 28 | 0, 8)) { - break label$1 - } - $4 = $0 + 4 | 0; - label$2 : { - label$3 : { - label$4 : { - label$5 : { - $5 = HEAP32[$7 + 28 >> 2]; - if (($5 | 0) == 255) { - break label$5 - } - HEAP8[$7 + 34 | 0] = $5; - HEAP32[$7 + 12 >> 2] = 3; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 28 | 0, 8)) { - break label$3 - } - $5 = HEAP32[$7 + 28 >> 2]; - if (($5 | 0) == 255) { - break label$5 - } - $8 = $2 >>> 1 & 1; - $2 = HEAP32[$7 + 12 >> 2]; - HEAP8[$2 + ($7 + 32 | 0) | 0] = $5; - $5 = 1; - HEAP32[$7 + 12 >> 2] = $2 + 1; - $2 = HEAPU8[$7 + 34 | 0]; - $3 = $2 >>> 4 | 0; - HEAP32[$7 + 28 >> 2] = $3; - label$6 : { - label$7 : { - label$8 : { - label$9 : { - switch ($3 - 1 | 0) { - case 7: - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 256 << $3 + -8; - break label$8; - case 1: - case 2: - case 3: - case 4: - HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 576 << $3 + -2; - break label$8; - case 5: - case 6: - break label$7; - case 0: - break label$9; - default: - break label$6; - }; - } - HEAP32[HEAP32[$4 >> 2] + 1136 >> 2] = 192; - } - $3 = 0; - } - $5 = $8; - } - $6 = $2 & 15; - HEAP32[$7 + 28 >> 2] = $6; - label$12 : { - label$13 : { - label$14 : { - switch ($6 - 1 | 0) { - default: - $6 = 0; - $8 = HEAP32[$4 >> 2]; - if (HEAP32[$8 + 248 >> 2]) { - break label$13 - } - $5 = 1; - break label$12; - case 0: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 88200; - $6 = 0; - break label$12; - case 1: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 176400; - $6 = 0; - break label$12; - case 2: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 192e3; - $6 = 0; - break label$12; - case 3: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 8e3; - $6 = 0; - break label$12; - case 4: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 16e3; - $6 = 0; - break label$12; - case 5: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 22050; - $6 = 0; - break label$12; - case 6: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 24e3; - $6 = 0; - break label$12; - case 7: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 32e3; - $6 = 0; - break label$12; - case 8: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 44100; - $6 = 0; - break label$12; - case 9: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 48e3; - $6 = 0; - break label$12; - case 10: - HEAP32[HEAP32[$4 >> 2] + 1140 >> 2] = 96e3; - $6 = 0; - break label$12; - case 11: - case 12: - case 13: - break label$12; - case 14: - break label$14; - }; - } - $5 = HEAP32[$4 >> 2]; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - break label$4; - } - HEAP32[$8 + 1140 >> 2] = HEAP32[$8 + 288 >> 2]; - } - $10 = HEAPU8[$7 + 35 | 0]; - $9 = $10 >>> 4 | 0; - HEAP32[$7 + 28 >> 2] = $9; - label$28 : { - label$29 : { - if ($9 & 8) { - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 1144 >> 2] = 2; - $8 = 1; - label$31 : { - switch ($9 & 7) { - case 1: - $8 = 2; - break label$29; - case 0: - break label$29; - case 2: - break label$31; - default: - break label$28; - }; - } - $8 = 3; - break label$29; - } - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 1144 >> 2] = $9 + 1; - $8 = 0; - } - HEAP32[$2 + 1148 >> 2] = $8; - $8 = $5; - } - $9 = $10 >>> 1 & 7; - HEAP32[$7 + 28 >> 2] = $9; - $5 = 1; - label$33 : { - label$34 : { - label$35 : { - switch ($9 - 1 | 0) { - default: - if (!HEAP32[$2 + 248 >> 2]) { - break label$33 - } - HEAP32[$2 + 1152 >> 2] = HEAP32[$2 + 296 >> 2]; - break label$34; - case 0: - HEAP32[$2 + 1152 >> 2] = 8; - break label$34; - case 1: - HEAP32[$2 + 1152 >> 2] = 12; - break label$34; - case 3: - HEAP32[$2 + 1152 >> 2] = 16; - break label$34; - case 4: - HEAP32[$2 + 1152 >> 2] = 20; - break label$34; - case 2: - case 6: - break label$33; - case 5: - break label$35; - }; - } - HEAP32[$2 + 1152 >> 2] = 24; - } - $5 = $8; - } - label$41 : { - if (!(!HEAP32[$2 + 248 >> 2] | HEAP32[$2 + 272 >> 2] == HEAP32[$2 + 276 >> 2] ? !(HEAP8[$7 + 33 | 0] & 1) : 0)) { - if (!FLAC__bitreader_read_utf8_uint64(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $7 + 32 | 0, $7 + 12 | 0)) { - break label$3 - } - $8 = HEAP32[$7 + 20 >> 2]; - $2 = $8; - $9 = HEAP32[$7 + 16 >> 2]; - if (($9 | 0) == -1 & ($2 | 0) == -1) { - $8 = HEAPU8[(HEAP32[$7 + 12 >> 2] + $7 | 0) + 31 | 0]; - $5 = HEAP32[$4 >> 2]; - HEAP32[$5 + 3520 >> 2] = 1; - HEAP8[$5 + 3590 | 0] = $8; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - break label$4; - } - $8 = HEAP32[$4 >> 2]; - $11 = $8 + 1160 | 0; - HEAP32[$11 >> 2] = $9; - HEAP32[$11 + 4 >> 2] = $2; - HEAP32[$8 + 1156 >> 2] = 1; - break label$41; - } - if (!FLAC__bitreader_read_utf8_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, $7 + 32 | 0, $7 + 12 | 0)) { - break label$3 - } - $8 = HEAP32[$7 + 28 >> 2]; - if (($8 | 0) == -1) { - $8 = HEAPU8[(HEAP32[$7 + 12 >> 2] + $7 | 0) + 31 | 0]; - $5 = HEAP32[$4 >> 2]; - HEAP32[$5 + 3520 >> 2] = 1; - HEAP8[$5 + 3590 | 0] = $8; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - break label$4; - } - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 1160 >> 2] = $8; - HEAP32[$2 + 1156 >> 2] = 0; - } - $2 = HEAP32[$4 >> 2]; - if ($3) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { - break label$3 - } - $2 = HEAP32[$7 + 12 >> 2]; - $8 = HEAP32[$7 + 28 >> 2]; - HEAP8[$2 + ($7 + 32 | 0) | 0] = $8; - HEAP32[$7 + 12 >> 2] = $2 + 1; - if (($3 | 0) == 7) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 8 | 0, 8)) { - break label$3 - } - $8 = HEAP32[$7 + 12 >> 2]; - $2 = HEAP32[$7 + 8 >> 2]; - HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; - HEAP32[$7 + 12 >> 2] = $8 + 1; - $8 = $2 | HEAP32[$7 + 28 >> 2] << 8; - HEAP32[$7 + 28 >> 2] = $8; - } - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 1136 >> 2] = $8 + 1; - } - if ($6) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { - break label$3 - } - $8 = HEAP32[$7 + 12 >> 2]; - $2 = HEAP32[$7 + 28 >> 2]; - HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; - HEAP32[$7 + 12 >> 2] = $8 + 1; - label$51 : { - if (($6 | 0) != 12) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 8 | 0, 8)) { - break label$3 - } - $8 = HEAP32[$7 + 12 >> 2]; - $2 = HEAP32[$7 + 8 >> 2]; - HEAP8[$8 + ($7 + 32 | 0) | 0] = $2; - HEAP32[$7 + 12 >> 2] = $8 + 1; - $3 = $2 | HEAP32[$7 + 28 >> 2] << 8; - HEAP32[$7 + 28 >> 2] = $3; - if (($6 | 0) == 13) { - break label$51 - } - $3 = Math_imul($3, 10); - break label$51; - } - $3 = Math_imul($2, 1e3); - } - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 1140 >> 2] = $3; - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { - break label$3 - } - $8 = HEAPU8[$7 + 28 | 0]; - $3 = FLAC__crc8($7 + 32 | 0, HEAP32[$7 + 12 >> 2]); - $2 = HEAP32[$4 >> 2]; - if (($3 | 0) != ($8 | 0)) { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 1, HEAP32[$2 + 48 >> 2]) - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - break label$4; - } - HEAP32[$2 + 232 >> 2] = 0; - label$55 : { - label$56 : { - if (HEAP32[$2 + 1156 >> 2]) { - break label$56 - } - $3 = $2 + 1160 | 0; - $8 = HEAP32[$3 >> 2]; - HEAP32[$7 + 28 >> 2] = $8; - HEAP32[$2 + 1156 >> 2] = 1; - $6 = HEAP32[$2 + 228 >> 2]; - if ($6) { - (wasm2js_i32$0 = $3, wasm2js_i32$1 = __wasm_i64_mul($6, 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - HEAP32[$3 + 4 >> 2] = i64toi32_i32$HIGH_BITS; - break label$56; - } - if (HEAP32[$2 + 248 >> 2]) { - $3 = HEAP32[$2 + 272 >> 2]; - if (($3 | 0) != HEAP32[$2 + 276 >> 2]) { - break label$55 - } - $2 = $2 + 1160 | 0; - (wasm2js_i32$0 = $2, wasm2js_i32$1 = __wasm_i64_mul($3, 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - HEAP32[$2 + 4 >> 2] = i64toi32_i32$HIGH_BITS; - $8 = HEAP32[$4 >> 2]; - HEAP32[$8 + 232 >> 2] = HEAP32[$8 + 276 >> 2]; - break label$56; - } - if (!$8) { - $8 = $2 + 1160 | 0; - HEAP32[$8 >> 2] = 0; - HEAP32[$8 + 4 >> 2] = 0; - $8 = HEAP32[$4 >> 2]; - HEAP32[$8 + 232 >> 2] = HEAP32[$8 + 1136 >> 2]; - break label$56; - } - $3 = $2 + 1160 | 0; - (wasm2js_i32$0 = $3, wasm2js_i32$1 = __wasm_i64_mul(HEAP32[$2 + 1136 >> 2], 0, $8, 0)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - HEAP32[$3 + 4 >> 2] = i64toi32_i32$HIGH_BITS; - } - if (!($5 | $10 & 1)) { - $2 = HEAP32[$0 >> 2]; - break label$4; - } - $2 = HEAP32[$4 >> 2]; - } - label$61 : { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); - break label$61; - } - HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - break label$4; - } - $5 = HEAP32[$4 >> 2]; - HEAP32[$5 + 3520 >> 2] = 1; - HEAP8[$5 + 3590 | 0] = 255; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 1, HEAP32[$5 + 48 >> 2]) - } - $2 = HEAP32[$0 >> 2]; - HEAP32[$2 >> 2] = 2; - } - $8 = 1; - if (HEAP32[$2 >> 2] == 2) { - break label$1 - } - $2 = HEAP32[$4 >> 2]; - $5 = HEAP32[$2 + 1144 >> 2]; - $6 = HEAP32[$2 + 1136 >> 2]; - if (!(HEAPU32[$2 + 224 >> 2] >= $5 >>> 0 ? HEAPU32[$2 + 220 >> 2] >= $6 >>> 0 : 0)) { - $3 = HEAP32[$2 + 60 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 60 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3592 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 92 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3592 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 - -64 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] - -64 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3596 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 96 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3596 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 68 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 68 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3600 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 100 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3600 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 72 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 72 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3604 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 104 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3604 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 76 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 76 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3608 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 108 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3608 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 80 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 80 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3612 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 112 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3612 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 84 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 84 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 3616 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$4 >> 2] + 116 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3616 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $3 = HEAP32[$2 + 88 >> 2]; - if ($3) { - dlfree($3 + -16 | 0); - HEAP32[HEAP32[$4 >> 2] + 88 >> 2] = 0; - $2 = HEAP32[$4 >> 2]; - } - $2 = HEAP32[$2 + 3620 >> 2]; - if ($2) { - dlfree($2); - HEAP32[HEAP32[$4 >> 2] + 120 >> 2] = 0; - HEAP32[HEAP32[$4 >> 2] + 3620 >> 2] = 0; - } - label$97 : { - if (!$5) { - break label$97 - } - if ($6 >>> 0 > 4294967291) { - break label$2 - } - $2 = $6 + 4 | 0; - if (($2 & 1073741823) != ($2 | 0)) { - break label$2 - } - $9 = $2 << 2; - $3 = 0; - while (1) { - $2 = dlmalloc($9); - if (!$2) { - break label$2 - } - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - HEAP32[$2 + 8 >> 2] = 0; - HEAP32[$2 + 12 >> 2] = 0; - $10 = $3 << 2; - HEAP32[($10 + HEAP32[$4 >> 2] | 0) + 60 >> 2] = $2 + 16; - $2 = $10 + HEAP32[$4 >> 2] | 0; - if (FLAC__memory_alloc_aligned_int32_array($6, $2 + 3592 | 0, $2 + 92 | 0)) { - $3 = $3 + 1 | 0; - if (($5 | 0) == ($3 | 0)) { - break label$97 - } - continue; - } - break; - }; - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$3; - } - $2 = HEAP32[$4 >> 2]; - HEAP32[$2 + 224 >> 2] = $5; - HEAP32[$2 + 220 >> 2] = $6; - $5 = HEAP32[$2 + 1144 >> 2]; - } - label$100 : { - if ($5) { - $17 = HEAP32[1412]; - $20 = -1 << $17 ^ -1; - $18 = HEAP32[1406]; - $19 = HEAP32[1405]; - $21 = HEAP32[1413]; - $5 = 0; - while (1) { - $3 = HEAP32[$2 + 1152 >> 2]; - label$103 : { - label$104 : { - switch (HEAP32[$2 + 1148 >> 2] + -1 | 0) { - case 0: - $3 = (($5 | 0) == 1) + $3 | 0; - break label$103; - case 1: - $3 = !$5 + $3 | 0; - break label$103; - case 2: - break label$104; - default: - break label$103; - }; - } - $3 = (($5 | 0) == 1) + $3 | 0; - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 28 | 0, 8)) { - break label$3 - } - $2 = HEAP32[$7 + 28 >> 2]; - HEAP32[$7 + 28 >> 2] = $2 & 254; - $13 = $2 & 1; - label$107 : { - if ($13) { - if (!FLAC__bitreader_read_unary_unsigned(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 32 | 0)) { - break label$3 - } - $2 = HEAP32[$4 >> 2]; - $6 = HEAP32[$7 + 32 >> 2] + 1 | 0; - HEAP32[($2 + Math_imul($5, 292) | 0) + 1464 >> 2] = $6; - if ($3 >>> 0 <= $6 >>> 0) { - break label$3 - } - $3 = $3 - $6 | 0; - break label$107; - } - $2 = HEAP32[$4 >> 2]; - HEAP32[($2 + Math_imul($5, 292) | 0) + 1464 >> 2] = 0; - } - $6 = HEAP32[$7 + 28 >> 2]; - label$109 : { - if ($6 & 128) { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$109; - } - label$112 : { - label$113 : { - label$114 : { - switch ($6 | 0) { - case 0: - $6 = HEAP32[(($5 << 2) + $2 | 0) + 60 >> 2]; - $9 = Math_imul($5, 292) + $2 | 0; - HEAP32[$9 + 1176 >> 2] = 0; - if (!FLAC__bitreader_read_raw_int32(HEAP32[$2 + 56 >> 2], $7 + 32 | 0, $3)) { - break label$3 - } - HEAP32[$9 + 1180 >> 2] = HEAP32[$7 + 32 >> 2]; - $2 = 0; - $3 = HEAP32[$4 >> 2]; - if (!HEAP32[$3 + 1136 >> 2]) { - break label$113 - } - while (1) { - HEAP32[$6 + ($2 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; - $2 = $2 + 1 | 0; - if ($2 >>> 0 < HEAPU32[$3 + 1136 >> 2]) { - continue - } - break; - }; - break label$113; - case 2: - $6 = ($2 + 1136 | 0) + Math_imul($5, 292) | 0; - $9 = $6 + 44 | 0; - $10 = $5 << 2; - $11 = HEAP32[($10 + $2 | 0) + 92 >> 2]; - HEAP32[$9 >> 2] = $11; - HEAP32[$6 + 40 >> 2] = 1; - $6 = 0; - if (HEAP32[$2 + 1136 >> 2]) { - while (1) { - if (!FLAC__bitreader_read_raw_int32(HEAP32[$2 + 56 >> 2], $7 + 32 | 0, $3)) { - break label$3 - } - HEAP32[$11 + ($6 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; - $6 = $6 + 1 | 0; - $2 = HEAP32[$4 >> 2]; - $12 = HEAP32[$2 + 1136 >> 2]; - if ($6 >>> 0 < $12 >>> 0) { - continue - } - break; - }; - $6 = $12 << 2; - } - memcpy(HEAP32[($2 + $10 | 0) + 60 >> 2], HEAP32[$9 >> 2], $6); - break label$113; - default: - break label$114; - }; - } - if ($6 >>> 0 <= 15) { - label$121 : { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); - break label$121; - } - HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$109; - } - if ($6 >>> 0 <= 24) { - $9 = Math_imul($5, 292) + $2 | 0; - HEAP32[$9 + 1176 >> 2] = 2; - $11 = $5 << 2; - $12 = HEAP32[($11 + $2 | 0) + 92 >> 2]; - $10 = $6 >>> 1 & 7; - HEAP32[$9 + 1192 >> 2] = $10; - HEAP32[$9 + 1212 >> 2] = $12; - $6 = HEAP32[$2 + 56 >> 2]; - if ($10) { - $12 = $9 + 1196 | 0; - $2 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, $3)) { - break label$3 - } - HEAP32[$12 + ($2 << 2) >> 2] = HEAP32[$7 + 32 >> 2]; - $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - $2 = $2 + 1 | 0; - if (($10 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $19)) { - break label$3 - } - $6 = $9 + 1180 | 0; - $3 = HEAP32[$7 + 16 >> 2]; - HEAP32[$6 >> 2] = $3; - $2 = HEAP32[$4 >> 2]; - label$126 : { - label$127 : { - if ($3 >>> 0 <= 1) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $18)) { - break label$3 - } - $2 = HEAP32[$4 >> 2]; - $3 = HEAP32[$7 + 16 >> 2]; - if (HEAP32[$2 + 1136 >> 2] >>> $3 >>> 0 >= $10 >>> 0) { - break label$127 - } - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$126; - } - label$130 : { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); - break label$130; - } - HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$126; - } - HEAP32[$9 + 1184 >> 2] = $3; - $2 = Math_imul($5, 12); - HEAP32[$9 + 1188 >> 2] = ($2 + HEAP32[$4 >> 2] | 0) + 124; - $6 = HEAP32[$6 >> 2]; - if ($6 >>> 0 < 2) { - $14 = $3; - $3 = HEAP32[$0 + 4 >> 2]; - if (!read_residual_partitioned_rice_($0, $10, $14, ($2 + $3 | 0) + 124 | 0, HEAP32[($3 + $11 | 0) + 92 >> 2], ($6 | 0) == 1)) { - break label$3 - } - } - $2 = $10 << 2; - memcpy(HEAP32[($11 + HEAP32[$4 >> 2] | 0) + 60 >> 2], $9 + 1196 | 0, $2); - $3 = HEAP32[$4 >> 2]; - $6 = $3 + $11 | 0; - FLAC__fixed_restore_signal(HEAP32[$6 + 92 >> 2], HEAP32[$3 + 1136 >> 2] - $10 | 0, $10, $2 + HEAP32[$6 + 60 >> 2] | 0); - } - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { - break label$109 - } - if ($13) { - break label$112 - } - break label$109; - } - if ($6 >>> 0 <= 63) { - label$134 : { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); - break label$134; - } - HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$109; - } - $9 = Math_imul($5, 292) + $2 | 0; - HEAP32[$9 + 1176 >> 2] = 3; - $11 = $5 << 2; - $15 = HEAP32[($11 + $2 | 0) + 92 >> 2]; - $12 = $6 >>> 1 & 31; - $10 = $12 + 1 | 0; - HEAP32[$9 + 1192 >> 2] = $10; - HEAP32[$9 + 1460 >> 2] = $15; - $6 = HEAP32[$2 + 56 >> 2]; - $2 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, $3)) { - break label$3 - } - HEAP32[($9 + ($2 << 2) | 0) + 1332 >> 2] = HEAP32[$7 + 32 >> 2]; - $15 = ($2 | 0) != ($12 | 0); - $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - $2 = $2 + 1 | 0; - if ($15) { - continue - } - break; - }; - if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $17)) { - break label$3 - } - $2 = HEAP32[$7 + 16 >> 2]; - label$137 : { - if (($2 | 0) == ($20 | 0)) { - $2 = HEAP32[$4 >> 2]; - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$137; - } - $16 = $9 + 1196 | 0; - HEAP32[$16 >> 2] = $2 + 1; - if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 32 | 0, $21)) { - break label$3 - } - $2 = HEAP32[$7 + 32 >> 2]; - if (($2 | 0) <= -1) { - $2 = HEAP32[$4 >> 2]; - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$137; - } - $15 = $9 + 1200 | 0; - HEAP32[$15 >> 2] = $2; - $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - $2 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_int32($6, $7 + 32 | 0, HEAP32[$16 >> 2])) { - break label$3 - } - HEAP32[($9 + ($2 << 2) | 0) + 1204 >> 2] = HEAP32[$7 + 32 >> 2]; - $14 = ($2 | 0) != ($12 | 0); - $6 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - $2 = $2 + 1 | 0; - if ($14) { - continue - } - break; - }; - if (!FLAC__bitreader_read_raw_uint32($6, $7 + 16 | 0, $19)) { - break label$3 - } - $14 = $9 + 1180 | 0; - $6 = HEAP32[$7 + 16 >> 2]; - HEAP32[$14 >> 2] = $6; - $2 = HEAP32[$4 >> 2]; - label$143 : { - if ($6 >>> 0 <= 1) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[$2 + 56 >> 2], $7 + 16 | 0, $18)) { - break label$3 - } - $2 = HEAP32[$4 >> 2]; - $6 = HEAP32[$7 + 16 >> 2]; - if (HEAP32[$2 + 1136 >> 2] >>> $6 >>> 0 > $12 >>> 0) { - break label$143 - } - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 0, HEAP32[$2 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$137; - } - label$146 : { - if (!HEAP32[$2 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$2 + 32 >> 2]]($0, 3, HEAP32[$2 + 48 >> 2]); - break label$146; - } - HEAP32[$2 + 6152 >> 2] = HEAP32[$2 + 6152 >> 2] + 1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$137; - } - HEAP32[$9 + 1184 >> 2] = $6; - $2 = Math_imul($5, 12); - HEAP32[$9 + 1188 >> 2] = ($2 + HEAP32[$4 >> 2] | 0) + 124; - $12 = HEAP32[$14 >> 2]; - if ($12 >>> 0 < 2) { - $14 = $6; - $6 = HEAP32[$0 + 4 >> 2]; - if (!read_residual_partitioned_rice_($0, $10, $14, ($2 + $6 | 0) + 124 | 0, HEAP32[($6 + $11 | 0) + 92 >> 2], ($12 | 0) == 1)) { - break label$3 - } - } - $6 = $10 << 2; - memcpy(HEAP32[(HEAP32[$4 >> 2] + $11 | 0) + 60 >> 2], $9 + 1332 | 0, $6); - label$149 : { - $12 = HEAP32[$16 >> 2]; - if ($12 + ((Math_clz32($10) ^ 31) + $3 | 0) >>> 0 <= 32) { - $2 = HEAP32[$4 >> 2]; - if ($3 >>> 0 > 16 | $12 >>> 0 > 16) { - break label$149 - } - $3 = $2 + $11 | 0; - FUNCTION_TABLE[HEAP32[$2 + 44 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); - break label$137; - } - $2 = HEAP32[$4 >> 2]; - $3 = $2 + $11 | 0; - FUNCTION_TABLE[HEAP32[$2 + 40 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); - break label$137; - } - $3 = $2 + $11 | 0; - FUNCTION_TABLE[HEAP32[$2 + 36 >> 2]](HEAP32[$3 + 92 >> 2], HEAP32[$2 + 1136 >> 2] - $10 | 0, $9 + 1204 | 0, $10, HEAP32[$15 >> 2], $6 + HEAP32[$3 + 60 >> 2] | 0); - } - if (!$13 | HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { - break label$109 - } - break label$112; - } - if (!$13) { - break label$109 - } - } - $3 = HEAP32[$4 >> 2]; - $2 = HEAP32[($3 + Math_imul($5, 292) | 0) + 1464 >> 2]; - HEAP32[$7 + 28 >> 2] = $2; - if (!HEAP32[$3 + 1136 >> 2]) { - break label$109 - } - $6 = HEAP32[($3 + ($5 << 2) | 0) + 60 >> 2]; - HEAP32[$6 >> 2] = HEAP32[$6 >> 2] << $2; - $2 = 1; - if (HEAPU32[$3 + 1136 >> 2] < 2) { - break label$109 - } - while (1) { - $9 = $6 + ($2 << 2) | 0; - HEAP32[$9 >> 2] = HEAP32[$9 >> 2] << HEAP32[$7 + 28 >> 2]; - $2 = $2 + 1 | 0; - if ($2 >>> 0 < HEAPU32[$3 + 1136 >> 2]) { - continue - } - break; - }; - } - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { - break label$100 - } - $5 = $5 + 1 | 0; - $2 = HEAP32[$4 >> 2]; - if ($5 >>> 0 < HEAPU32[$2 + 1144 >> 2]) { - continue - } - break; - }; - } - label$152 : { - if (FLAC__bitreader_is_consumed_byte_aligned(HEAP32[$2 + 56 >> 2])) { - break label$152 - } - HEAP32[$7 + 32 >> 2] = 0; - $5 = HEAP32[HEAP32[$4 >> 2] + 56 >> 2]; - if (!FLAC__bitreader_read_raw_uint32($5, $7 + 32 | 0, FLAC__bitreader_bits_left_for_byte_alignment($5))) { - break label$3 - } - if (!HEAP32[$7 + 32 >> 2]) { - break label$152 - } - $5 = HEAP32[$4 >> 2]; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 0, HEAP32[$5 + 48 >> 2]) - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - } - if (HEAP32[HEAP32[$0 >> 2] >> 2] == 2) { - break label$1 - } - $5 = FLAC__bitreader_get_read_crc16(HEAP32[HEAP32[$4 >> 2] + 56 >> 2]); - $8 = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$4 >> 2] + 56 >> 2], $7 + 16 | 0, HEAP32[1404])) { - break label$1 - } - label$154 : { - if (($5 | 0) == HEAP32[$7 + 16 >> 2]) { - label$156 : { - label$157 : { - label$158 : { - $5 = HEAP32[$4 >> 2]; - switch (HEAP32[$5 + 1148 >> 2] + -1 | 0) { - case 2: - break label$156; - case 0: - break label$157; - case 1: - break label$158; - default: - break label$154; - }; - } - if (!HEAP32[$5 + 1136 >> 2]) { - break label$154 - } - $2 = HEAP32[$5 - -64 >> 2]; - $6 = HEAP32[$5 + 60 >> 2]; - $3 = 0; - while (1) { - $9 = $3 << 2; - $10 = $9 + $6 | 0; - HEAP32[$10 >> 2] = HEAP32[$10 >> 2] + HEAP32[$2 + $9 >> 2]; - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { - continue - } - break; - }; - break label$154; - } - if (!HEAP32[$5 + 1136 >> 2]) { - break label$154 - } - $2 = HEAP32[$5 - -64 >> 2]; - $6 = HEAP32[$5 + 60 >> 2]; - $3 = 0; - while (1) { - $9 = $3 << 2; - $10 = $9 + $2 | 0; - HEAP32[$10 >> 2] = HEAP32[$6 + $9 >> 2] - HEAP32[$10 >> 2]; - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { - continue - } - break; - }; - break label$154; - } - if (!HEAP32[$5 + 1136 >> 2]) { - break label$154 - } - $10 = HEAP32[$5 - -64 >> 2]; - $11 = HEAP32[$5 + 60 >> 2]; - $3 = 0; - while (1) { - $6 = $3 << 2; - $2 = $6 + $11 | 0; - $13 = $6 + $10 | 0; - $6 = HEAP32[$13 >> 2]; - $9 = $6 & 1 | HEAP32[$2 >> 2] << 1; - HEAP32[$2 >> 2] = $6 + $9 >> 1; - HEAP32[$13 >> 2] = $9 - $6 >> 1; - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$5 + 1136 >> 2]) { - continue - } - break; - }; - break label$154; - } - $5 = HEAP32[$4 >> 2]; - if (!HEAP32[$5 + 3632 >> 2]) { - FUNCTION_TABLE[HEAP32[$5 + 32 >> 2]]($0, 2, HEAP32[$5 + 48 >> 2]) - } - $2 = HEAP32[$4 >> 2]; - if (!HEAP32[$2 + 1144 >> 2]) { - break label$154 - } - $3 = 0; - while (1) { - memset(HEAP32[(($3 << 2) + $2 | 0) + 60 >> 2], HEAP32[$2 + 1136 >> 2] << 2); - $3 = $3 + 1 | 0; - $2 = HEAP32[$4 >> 2]; - if ($3 >>> 0 < HEAPU32[$2 + 1144 >> 2]) { - continue - } - break; - }; - } - HEAP32[$1 >> 2] = 1; - $2 = HEAP32[$4 >> 2]; - $1 = HEAP32[$2 + 232 >> 2]; - if ($1) { - HEAP32[$2 + 228 >> 2] = $1 - } - $1 = HEAP32[$0 >> 2]; - $6 = HEAP32[$2 + 1144 >> 2]; - HEAP32[$1 + 8 >> 2] = $6; - HEAP32[$1 + 12 >> 2] = HEAP32[$2 + 1148 >> 2]; - $13 = HEAP32[$2 + 1152 >> 2]; - HEAP32[$1 + 16 >> 2] = $13; - HEAP32[$1 + 20 >> 2] = HEAP32[$2 + 1140 >> 2]; - $5 = HEAP32[$2 + 1136 >> 2]; - HEAP32[$1 + 24 >> 2] = $5; - $1 = $2 + 1160 | 0; - $9 = HEAP32[$1 >> 2]; - $3 = HEAP32[$1 + 4 >> 2]; - $1 = $3; - $12 = $5 + $9 | 0; - if ($12 >>> 0 < $5 >>> 0) { - $1 = $1 + 1 | 0 - } - HEAP32[$2 + 240 >> 2] = $12; - HEAP32[$2 + 244 >> 2] = $1; - $10 = $2 + 60 | 0; - $11 = $2 + 1136 | 0; - label$165 : { - label$166 : { - label$167 : { - if (HEAP32[$2 + 3632 >> 2]) { - HEAP32[$2 + 6156 >> 2] = 1; - $13 = HEAP32[$2 + 6144 >> 2]; - $5 = HEAP32[$2 + 6148 >> 2]; - memcpy($2 + 3752 | 0, $11, 2384); - if (($3 | 0) == ($5 | 0) & $13 >>> 0 < $9 >>> 0 | $5 >>> 0 < $3 >>> 0 | (($1 | 0) == ($5 | 0) & $13 >>> 0 >= $12 >>> 0 | $5 >>> 0 > $1 >>> 0)) { - break label$165 - } - $3 = 0; - $1 = HEAP32[$4 >> 2]; - HEAP32[$1 + 3632 >> 2] = 0; - $5 = $13 - $9 | 0; - $4 = $5; - if ($4) { - if ($6) { - while (1) { - $9 = $3 << 2; - HEAP32[$9 + ($7 + 32 | 0) >> 2] = HEAP32[($2 + $9 | 0) + 60 >> 2] + ($4 << 2); - $3 = $3 + 1 | 0; - if (($6 | 0) != ($3 | 0)) { - continue - } - break; - } - } - HEAP32[$1 + 3752 >> 2] = HEAP32[$1 + 3752 >> 2] - $4; - $2 = $1 + 3776 | 0; - $4 = $2; - $3 = $2; - $1 = HEAP32[$2 + 4 >> 2]; - $2 = $5 + HEAP32[$2 >> 2] | 0; - if ($2 >>> 0 < $5 >>> 0) { - $1 = $1 + 1 | 0 - } - HEAP32[$3 >> 2] = $2; - HEAP32[$4 + 4 >> 2] = $1; - $1 = HEAP32[$0 + 4 >> 2]; - $1 = FUNCTION_TABLE[HEAP32[$1 + 24 >> 2]]($0, $1 + 3752 | 0, $7 + 32 | 0, HEAP32[$1 + 48 >> 2]) | 0; - break label$167; - } - $1 = FUNCTION_TABLE[HEAP32[$1 + 24 >> 2]]($0, $11, $10, HEAP32[$1 + 48 >> 2]) | 0; - break label$167; - } - label$172 : { - if (!HEAP32[$2 + 248 >> 2]) { - HEAP32[$2 + 3624 >> 2] = 0; - break label$172; - } - if (!HEAP32[$2 + 3624 >> 2]) { - break label$172 - } - if (!FLAC__MD5Accumulate($2 + 3636 | 0, $10, $6, $5, $13 + 7 >>> 3 | 0)) { - break label$166 - } - $2 = HEAP32[$4 >> 2]; - } - $1 = FUNCTION_TABLE[HEAP32[$2 + 24 >> 2]]($0, $11, $10, HEAP32[$2 + 48 >> 2]) | 0; - } - if (!$1) { - break label$165 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - } - $8 = 1; - break label$1; - } - $8 = 0; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $8 = 0; - } - global$0 = $7 - -64 | 0; - return $8; - } - - function read_residual_partitioned_rice_($0, $1, $2, $3, $4, $5) { - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; - $6 = global$0 - 16 | 0; - global$0 = $6; - $7 = HEAP32[HEAP32[$0 + 4 >> 2] + 1136 >> 2]; - $11 = HEAP32[($5 ? 5644 : 5640) >> 2]; - $12 = HEAP32[($5 ? 5632 : 5628) >> 2]; - label$1 : { - label$2 : { - if (FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($3, $2 >>> 0 > 6 ? $2 : 6)) { - $8 = $2 ? $7 >>> $2 | 0 : $7 - $1 | 0; - $13 = HEAP32[1409]; - if (!$2) { - break label$2 - } - $5 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $12)) { - $7 = 0; - break label$1; - } - $9 = $10 << 2; - HEAP32[$9 + HEAP32[$3 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; - label$6 : { - if (HEAPU32[$6 + 12 >> 2] < $11 >>> 0) { - $7 = 0; - HEAP32[$9 + HEAP32[$3 + 4 >> 2] >> 2] = 0; - $9 = $8 - ($10 ? 0 : $1) | 0; - if (!FLAC__bitreader_read_rice_signed_block(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], ($5 << 2) + $4 | 0, $9, HEAP32[$6 + 12 >> 2])) { - break label$1 - } - $5 = $5 + $9 | 0; - break label$6; - } - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $13)) { - $7 = 0; - break label$1; - } - HEAP32[$9 + HEAP32[$3 + 4 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; - $7 = $10 ? 0 : $1; - if ($7 >>> 0 >= $8 >>> 0) { - break label$6 - } - while (1) { - if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 8 | 0, HEAP32[$6 + 12 >> 2])) { - $7 = 0; - break label$1; - } - HEAP32[($5 << 2) + $4 >> 2] = HEAP32[$6 + 8 >> 2]; - $5 = $5 + 1 | 0; - $7 = $7 + 1 | 0; - if (($8 | 0) != ($7 | 0)) { - continue - } - break; - }; - } - $7 = 1; - $10 = $10 + 1 | 0; - if (!($10 >>> $2)) { - continue - } - break; - }; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $7 = 0; - break label$1; - } - $7 = 0; - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $12)) { - break label$1 - } - HEAP32[HEAP32[$3 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; - label$11 : { - if (HEAPU32[$6 + 12 >> 2] >= $11 >>> 0) { - if (!FLAC__bitreader_read_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 12 | 0, $13)) { - break label$1 - } - HEAP32[HEAP32[$3 + 4 >> 2] >> 2] = HEAP32[$6 + 12 >> 2]; - if (!$8) { - break label$11 - } - $5 = 0; - while (1) { - if (!FLAC__bitreader_read_raw_int32(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $6 + 8 | 0, HEAP32[$6 + 12 >> 2])) { - $7 = 0; - break label$1; - } - HEAP32[($5 << 2) + $4 >> 2] = HEAP32[$6 + 8 >> 2]; - $5 = $5 + 1 | 0; - $7 = $7 + 1 | 0; - if (($8 | 0) != ($7 | 0)) { - continue - } - break; - }; - break label$11; - } - HEAP32[HEAP32[$3 + 4 >> 2] >> 2] = 0; - if (!FLAC__bitreader_read_rice_signed_block(HEAP32[HEAP32[$0 + 4 >> 2] + 56 >> 2], $4, $8, HEAP32[$6 + 12 >> 2])) { - break label$1 - } - } - $7 = 1; - } - global$0 = $6 + 16 | 0; - return $7; - } - - function FLAC__stream_decoder_process_until_end_of_metadata($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0; - label$1 : { - label$2 : { - while (1) { - label$4 : { - $1 = 1; - label$5 : { - switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { - case 0: - if (find_metadata_($0)) { - continue - } - break label$4; - case 2: - case 3: - case 4: - case 7: - break label$2; - case 1: - break label$5; - default: - break label$1; - }; - } - if (read_metadata_($0)) { - continue - } - } - break; - }; - $1 = 0; - } - $2 = $1; - } - return $2 | 0; - } - - function FLAC__stream_decoder_process_until_end_of_stream($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0; - $1 = global$0 - 16 | 0; - global$0 = $1; - $2 = 1; - label$1 : { - label$2 : { - while (1) { - label$4 : { - label$5 : { - switch (HEAP32[HEAP32[$0 >> 2] >> 2]) { - case 0: - if (find_metadata_($0)) { - continue - } - break label$4; - case 1: - if (read_metadata_($0)) { - continue - } - break label$4; - case 2: - if (frame_sync_($0)) { - continue - } - break label$2; - case 4: - case 7: - break label$2; - case 3: - break label$5; - default: - break label$1; - }; - } - if (read_frame_($0, $1 + 12 | 0)) { - continue - } - } - break; - }; - $2 = 0; - } - $3 = $2; - } - global$0 = $1 + 16 | 0; - return $3 | 0; - } - - function read_callback_proxy_($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $0 = FUNCTION_TABLE[HEAP32[HEAP32[$0 + 4 >> 2] + 4 >> 2]]($0, $1, $2, $3) | 0; - if ($0 >>> 0 <= 2) { - return HEAP32[($0 << 2) + 7572 >> 2] - } - return 5; - } - - function FLAC__bitwriter_free($0) { - var $1 = 0; - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - } - - function FLAC__bitwriter_init($0) { - var $1 = 0; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 8192; - HEAP32[$0 + 12 >> 2] = 0; - $1 = $0; - $0 = dlmalloc(32768); - HEAP32[$1 >> 2] = $0; - return ($0 | 0) != 0; - } - - function FLAC__bitwriter_clear($0) { - HEAP32[$0 + 12 >> 2] = 0; - HEAP32[$0 + 16 >> 2] = 0; - } - - function FLAC__bitwriter_get_write_crc16($0, $1) { - var $2 = 0, $3 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - $3 = 0; - label$1 : { - if (!FLAC__bitwriter_get_buffer($0, $2 + 12 | 0, $2 + 8 | 0)) { - break label$1 - } - (wasm2js_i32$0 = $1, wasm2js_i32$1 = FLAC__crc16(HEAP32[$2 + 12 >> 2], HEAP32[$2 + 8 >> 2])), HEAP16[wasm2js_i32$0 >> 1] = wasm2js_i32$1; - $3 = 1; - } - global$0 = $2 + 16 | 0; - return $3; - } - - function FLAC__bitwriter_get_buffer($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $5 = HEAP32[$0 + 16 >> 2]; - label$1 : { - if ($5 & 7) { - break label$1 - } - label$2 : { - if (!$5) { - $4 = HEAP32[$0 >> 2]; - $3 = 0; - break label$2; - } - $6 = HEAP32[$0 + 12 >> 2]; - label$4 : { - if (($6 | 0) != HEAP32[$0 + 8 >> 2]) { - break label$4 - } - $4 = $5 + 63 >>> 5 | 0; - $3 = $4 + $6 | 0; - if ($3 >>> 0 <= $6 >>> 0) { - break label$4 - } - $6 = 0; - $5 = HEAP32[$0 >> 2]; - $7 = $3; - $3 = $4 & 1023; - $3 = $7 + ($3 ? 1024 - $3 | 0 : 0) | 0; - label$5 : { - if ($3) { - if (($3 | 0) != ($3 & 1073741823)) { - break label$1 - } - $4 = dlrealloc($5, $3 << 2); - if ($4) { - break label$5 - } - dlfree($5); - return 0; - } - $4 = dlrealloc($5, 0); - if (!$4) { - break label$1 - } - } - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 >> 2] = $4; - $6 = HEAP32[$0 + 12 >> 2]; - $5 = HEAP32[$0 + 16 >> 2]; - } - $4 = HEAP32[$0 >> 2]; - $3 = HEAP32[$0 + 4 >> 2] << 32 - $5; - HEAP32[$4 + ($6 << 2) >> 2] = $3 << 24 | $3 << 8 & 16711680 | ($3 >>> 8 & 65280 | $3 >>> 24); - $3 = HEAP32[$0 + 16 >> 2] >>> 3 | 0; - } - HEAP32[$1 >> 2] = $4; - HEAP32[$2 >> 2] = $3 + (HEAP32[$0 + 12 >> 2] << 2); - $6 = 1; - } - return $6; - } - - function FLAC__bitwriter_get_write_crc8($0, $1) { - var $2 = 0, $3 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - $3 = 0; - label$1 : { - if (!FLAC__bitwriter_get_buffer($0, $2 + 12 | 0, $2 + 8 | 0)) { - break label$1 - } - (wasm2js_i32$0 = $1, wasm2js_i32$1 = FLAC__crc8(HEAP32[$2 + 12 >> 2], HEAP32[$2 + 8 >> 2])), HEAP8[wasm2js_i32$0 | 0] = wasm2js_i32$1; - $3 = 1; - } - global$0 = $2 + 16 | 0; - return $3; - } - - function FLAC__bitwriter_write_zeroes($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0; - label$1 : { - label$2 : { - if (!$1) { - break label$2 - } - $2 = HEAP32[$0 + 8 >> 2]; - $3 = HEAP32[$0 + 12 >> 2]; - label$3 : { - if ($2 >>> 0 > $3 + $1 >>> 0) { - break label$3 - } - $4 = $3 + ((HEAP32[$0 + 16 >> 2] + $1 | 0) + 31 >>> 5 | 0) | 0; - if ($4 >>> 0 <= $2 >>> 0) { - break label$3 - } - $3 = 0; - $5 = HEAP32[$0 >> 2]; - $2 = $4 - $2 & 1023; - $2 = $4 + ($2 ? 1024 - $2 | 0 : 0) | 0; - label$4 : { - if ($2) { - if (($2 | 0) != ($2 & 1073741823)) { - break label$1 - } - $4 = dlrealloc($5, $2 << 2); - if ($4) { - break label$4 - } - dlfree($5); - return 0; - } - $4 = dlrealloc($5, 0); - if (!$4) { - break label$1 - } - } - HEAP32[$0 + 8 >> 2] = $2; - HEAP32[$0 >> 2] = $4; - } - $2 = HEAP32[$0 + 16 >> 2]; - if ($2) { - $4 = $2; - $2 = 32 - $2 | 0; - $3 = $2 >>> 0 < $1 >>> 0 ? $2 : $1; - $5 = $4 + $3 | 0; - HEAP32[$0 + 16 >> 2] = $5; - $2 = HEAP32[$0 + 4 >> 2] << $3; - HEAP32[$0 + 4 >> 2] = $2; - if (($5 | 0) != 32) { - break label$2 - } - $5 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $5 + 1; - HEAP32[HEAP32[$0 >> 2] + ($5 << 2) >> 2] = $2 << 8 & 16711680 | $2 << 24 | ($2 >>> 8 & 65280 | $2 >>> 24); - HEAP32[$0 + 16 >> 2] = 0; - $1 = $1 - $3 | 0; - } - if ($1 >>> 0 >= 32) { - $2 = HEAP32[$0 >> 2]; - while (1) { - $3 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $3 + 1; - HEAP32[$2 + ($3 << 2) >> 2] = 0; - $1 = $1 + -32 | 0; - if ($1 >>> 0 > 31) { - continue - } - break; - }; - } - if (!$1) { - break label$2 - } - HEAP32[$0 + 16 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = 0; - } - $3 = 1; - } - return $3; - } - - function FLAC__bitwriter_write_raw_uint32($0, $1, $2) { - var $3 = 0; - label$1 : { - if ($2 >>> 0 <= 31) { - $3 = 0; - if ($1 >>> $2) { - break label$1 - } - } - $3 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $2); - } - return $3; - } - - function FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - label$1 : { - if (!$0 | $2 >>> 0 > 32) { - break label$1 - } - $4 = HEAP32[$0 >> 2]; - if (!$4) { - break label$1 - } - $6 = 1; - if (!$2) { - break label$1 - } - $7 = HEAP32[$0 + 8 >> 2]; - $3 = HEAP32[$0 + 12 >> 2]; - label$2 : { - if ($7 >>> 0 > $3 + $2 >>> 0) { - $3 = $4; - break label$2; - } - $5 = $3 + ((HEAP32[$0 + 16 >> 2] + $2 | 0) + 31 >>> 5 | 0) | 0; - if ($5 >>> 0 <= $7 >>> 0) { - $3 = $4; - break label$2; - } - $6 = 0; - $3 = $5 - $7 & 1023; - $5 = $5 + ($3 ? 1024 - $3 | 0 : 0) | 0; - label$5 : { - if ($5) { - if (($5 | 0) != ($5 & 1073741823)) { - break label$1 - } - $3 = dlrealloc($4, $5 << 2); - if ($3) { - break label$5 - } - dlfree($4); - return 0; - } - $3 = dlrealloc($4, 0); - if (!$3) { - break label$1 - } - } - HEAP32[$0 + 8 >> 2] = $5; - HEAP32[$0 >> 2] = $3; - } - $4 = HEAP32[$0 + 16 >> 2]; - $5 = 32 - $4 | 0; - if ($5 >>> 0 > $2 >>> 0) { - HEAP32[$0 + 16 >> 2] = $2 + $4; - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] << $2 | $1; - return 1; - } - if ($4) { - $4 = $2 - $5 | 0; - HEAP32[$0 + 16 >> 2] = $4; - $2 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $2 + 1; - $3 = ($2 << 2) + $3 | 0; - $2 = HEAP32[$0 + 4 >> 2] << $5 | $1 >>> $4; - HEAP32[$3 >> 2] = $2 << 24 | $2 << 8 & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); - HEAP32[$0 + 4 >> 2] = $1; - return 1; - } - $6 = 1; - $2 = $0; - $0 = HEAP32[$0 + 12 >> 2]; - HEAP32[$2 + 12 >> 2] = $0 + 1; - HEAP32[($0 << 2) + $3 >> 2] = $1 << 8 & 16711680 | $1 << 24 | ($1 >>> 8 & 65280 | $1 >>> 24); - } - return $6; - } - - function FLAC__bitwriter_write_raw_int32($0, $1, $2) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 >>> 0 < 32 ? -1 << $2 ^ -1 : -1) & $1, $2); - } - - function FLAC__bitwriter_write_raw_uint64($0, $1, $2, $3) { - var $4 = 0; - label$1 : { - if ($3 >>> 0 >= 33) { - $3 = $3 + -32 | 0; - if ($2 >>> $3 | 0 ? $3 >>> 0 <= 31 : 0) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $2, $3)) { - break label$1 - } - return (FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 32) | 0) != 0; - } - if (($3 | 0) != 32) { - if ($1 >>> $3) { - break label$1 - } - } - $4 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, $3); - } - return $4; - } - - function FLAC__bitwriter_write_raw_uint32_little_endian($0, $1) { - var $2 = 0; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 255, 8)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 8 & 255, 8)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 16 & 255, 8)) { - break label$1 - } - $2 = (FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 | 0, 8) | 0) != 0; - } - return $2; - } - - function FLAC__bitwriter_write_byte_block($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0; - $3 = HEAP32[$0 + 8 >> 2]; - $4 = HEAP32[$0 + 12 >> 2]; - label$1 : { - label$2 : { - if ($3 >>> 0 > ($4 + ($2 >>> 2 | 0) | 0) + 1 >>> 0) { - break label$2 - } - $5 = $4 + ((HEAP32[$0 + 16 >> 2] + ($2 << 3) | 0) + 31 >>> 5 | 0) | 0; - if ($5 >>> 0 <= $3 >>> 0) { - break label$2 - } - $4 = 0; - $6 = HEAP32[$0 >> 2]; - $3 = $5 - $3 & 1023; - $3 = $5 + ($3 ? 1024 - $3 | 0 : 0) | 0; - label$3 : { - if ($3) { - if (($3 | 0) != ($3 & 1073741823)) { - break label$1 - } - $5 = dlrealloc($6, $3 << 2); - if ($5) { - break label$3 - } - dlfree($6); - return 0; - } - $5 = dlrealloc($6, 0); - if (!$5) { - break label$1 - } - } - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 >> 2] = $5; - } - $4 = 1; - if (!$2) { - break label$1 - } - $4 = 0; - label$5 : { - while (1) { - if (!FLAC__bitwriter_write_raw_uint32_nocheck($0, HEAPU8[$1 + $4 | 0], 8)) { - break label$5 - } - $4 = $4 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - return 1; - } - $4 = 0; - } - return $4; - } - - function FLAC__bitwriter_write_unary_unsigned($0, $1) { - if ($1 >>> 0 <= 31) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, 1, $1 + 1 | 0) - } - if (!FLAC__bitwriter_write_zeroes($0, $1)) { - return 0 - } - return (FLAC__bitwriter_write_raw_uint32_nocheck($0, 1, 1) | 0) != 0; - } - - function FLAC__bitwriter_write_rice_signed_block($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0; - $4 = 1; - label$1 : { - if (!$2) { - break label$1 - } - $10 = $3 + 1 | 0; - $11 = -1 << $3; - $12 = -1 >>> 31 - $3 | 0; - while (1) { - $6 = HEAP32[$1 >> 2]; - $9 = $6 << 1 ^ $6 >> 31; - $6 = $9 >>> $3 | 0; - $4 = $10 + $6 | 0; - label$3 : { - label$4 : { - $5 = HEAP32[$0 + 16 >> 2]; - if (!$5) { - break label$4 - } - $7 = $4 + $5 | 0; - if ($7 >>> 0 > 31) { - break label$4 - } - HEAP32[$0 + 16 >> 2] = $7; - HEAP32[$0 + 4 >> 2] = ($9 | $11) & $12 | HEAP32[$0 + 4 >> 2] << $4; - break label$3; - } - $8 = HEAP32[$0 + 8 >> 2]; - $7 = HEAP32[$0 + 12 >> 2]; - label$5 : { - if ($8 >>> 0 > ($7 + ($5 + $6 | 0) | 0) + 1 >>> 0) { - break label$5 - } - $4 = $7 + (($4 + $5 | 0) + 31 >>> 5 | 0) | 0; - if ($4 >>> 0 <= $8 >>> 0) { - break label$5 - } - $7 = HEAP32[$0 >> 2]; - $5 = $4 - $8 & 1023; - $5 = $4 + ($5 ? 1024 - $5 | 0 : 0) | 0; - label$6 : { - if ($5) { - $4 = 0; - if (($5 | 0) != ($5 & 1073741823)) { - break label$1 - } - $8 = dlrealloc($7, $5 << 2); - if ($8) { - break label$6 - } - dlfree($7); - return 0; - } - $8 = dlrealloc($7, 0); - $4 = 0; - if (!$8) { - break label$1 - } - } - HEAP32[$0 + 8 >> 2] = $5; - HEAP32[$0 >> 2] = $8; - } - label$8 : { - if (!$6) { - break label$8 - } - $4 = HEAP32[$0 + 16 >> 2]; - if ($4) { - $5 = HEAP32[$0 + 4 >> 2]; - $7 = 32 - $4 | 0; - if ($6 >>> 0 < $7 >>> 0) { - HEAP32[$0 + 16 >> 2] = $4 + $6; - HEAP32[$0 + 4 >> 2] = $5 << $6; - break label$8; - } - $4 = $5 << $7; - HEAP32[$0 + 4 >> 2] = $4; - $5 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $5 + 1; - HEAP32[HEAP32[$0 >> 2] + ($5 << 2) >> 2] = $4 << 8 & 16711680 | $4 << 24 | ($4 >>> 8 & 65280 | $4 >>> 24); - HEAP32[$0 + 16 >> 2] = 0; - $6 = $6 - $7 | 0; - } - if ($6 >>> 0 >= 32) { - $4 = HEAP32[$0 >> 2]; - while (1) { - $5 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $5 + 1; - HEAP32[$4 + ($5 << 2) >> 2] = 0; - $6 = $6 + -32 | 0; - if ($6 >>> 0 > 31) { - continue - } - break; - }; - } - if (!$6) { - break label$8 - } - HEAP32[$0 + 16 >> 2] = $6; - HEAP32[$0 + 4 >> 2] = 0; - } - $6 = ($9 | $11) & $12; - $4 = HEAP32[$0 + 4 >> 2]; - $7 = HEAP32[$0 + 16 >> 2]; - $5 = 32 - $7 | 0; - if ($10 >>> 0 < $5 >>> 0) { - HEAP32[$0 + 16 >> 2] = $7 + $10; - HEAP32[$0 + 4 >> 2] = $6 | $4 << $10; - break label$3; - } - $7 = $10 - $5 | 0; - HEAP32[$0 + 16 >> 2] = $7; - $9 = HEAP32[$0 + 12 >> 2]; - HEAP32[$0 + 12 >> 2] = $9 + 1; - $4 = $4 << $5 | $6 >>> $7; - HEAP32[HEAP32[$0 >> 2] + ($9 << 2) >> 2] = $4 << 24 | $4 << 8 & 16711680 | ($4 >>> 8 & 65280 | $4 >>> 24); - HEAP32[$0 + 4 >> 2] = $6; - } - $1 = $1 + 4 | 0; - $2 = $2 + -1 | 0; - if ($2) { - continue - } - break; - }; - $4 = 1; - } - return $4; - } - - function FLAC__bitwriter_write_utf8_uint32($0, $1) { - if (($1 | 0) >= 0) { - if ($1 >>> 0 <= 127) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 8) - } - if ($1 >>> 0 <= 2047) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 | 192, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if ($1 >>> 0 <= 65535) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 | 224, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if ($1 >>> 0 <= 2097151) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 | 240, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if ($1 >>> 0 <= 67108863) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 | 248, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - $0 = FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 30 | 252, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1; - } else { - $0 = 0 - } - return $0; - } - - function FLAC__bitwriter_write_utf8_uint64($0, $1, $2) { - if (($2 | 0) == 15 | $2 >>> 0 < 15) { - if (!$2 & $1 >>> 0 <= 127 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, $1, 8) - } - if (!$2 & $1 >>> 0 <= 2047 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 63) << 26 | $1 >>> 6 | 192, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if (!$2 & $1 >>> 0 <= 65535 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 4095) << 20 | $1 >>> 12 | 224, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if (!$2 & $1 >>> 0 <= 2097151 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 262143) << 14 | $1 >>> 18 | 240, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if (!$2 & $1 >>> 0 <= 67108863 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 16777215) << 8 | $1 >>> 24 | 248, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - if (!$2 & $1 >>> 0 <= 2147483647 | $2 >>> 0 < 0) { - return FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 1073741823) << 2 | $1 >>> 30 | 252, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1 - } - $0 = FLAC__bitwriter_write_raw_uint32_nocheck($0, 254, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, ($2 & 1073741823) << 2 | $1 >>> 30 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 24 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 18 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 12 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 >>> 6 & 63 | 128, 8) & FLAC__bitwriter_write_raw_uint32_nocheck($0, $1 & 63 | 128, 8) & 1; - } else { - $0 = 0 - } - return $0; - } - - function FLAC__ogg_encoder_aspect_init($0) { - if (ogg_stream_init($0 + 8 | 0, HEAP32[$0 >> 2])) { - $0 = 0 - } else { - HEAP32[$0 + 392 >> 2] = 0; - HEAP32[$0 + 396 >> 2] = 0; - HEAP32[$0 + 384 >> 2] = 0; - HEAP32[$0 + 388 >> 2] = 1; - $0 = 1; - } - return $0; - } - - function FLAC__ogg_encoder_aspect_set_defaults($0) { - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - } - - function FLAC__ogg_encoder_aspect_write_callback_wrapper($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; - $9 = global$0 - 96 | 0; - global$0 = $9; - label$1 : { - label$2 : { - if (HEAP32[$0 + 384 >> 2]) { - HEAP32[$9 + 72 >> 2] = 0; - HEAP32[$9 + 76 >> 2] = 0; - $12 = $9 + 80 | 0; - $11 = $12; - HEAP32[$11 >> 2] = 0; - HEAP32[$11 + 4 >> 2] = 0; - HEAP32[$9 + 88 >> 2] = 0; - HEAP32[$9 + 92 >> 2] = 0; - HEAP32[$9 + 64 >> 2] = 0; - HEAP32[$9 + 68 >> 2] = 0; - $10 = HEAP32[$0 + 396 >> 2]; - $11 = $3; - $13 = HEAP32[$0 + 392 >> 2]; - $14 = $11 + $13 | 0; - if ($14 >>> 0 < $13 >>> 0) { - $10 = $10 + 1 | 0 - } - HEAP32[$12 >> 2] = $14; - HEAP32[$12 + 4 >> 2] = $10; - label$4 : { - label$5 : { - if (HEAP32[$0 + 388 >> 2]) { - if (($2 | 0) != 38) { - break label$4 - } - HEAP8[$9 | 0] = HEAPU8[7536]; - $2 = HEAP32[2721]; - $2 = HEAPU8[$2 | 0] | HEAPU8[$2 + 1 | 0] << 8 | (HEAPU8[$2 + 2 | 0] << 16 | HEAPU8[$2 + 3 | 0] << 24); - HEAP8[$9 + 5 | 0] = 1; - HEAP8[$9 + 6 | 0] = 0; - HEAP8[$9 + 1 | 0] = $2; - HEAP8[$9 + 2 | 0] = $2 >>> 8; - HEAP8[$9 + 3 | 0] = $2 >>> 16; - HEAP8[$9 + 4 | 0] = $2 >>> 24; - $10 = HEAP32[$0 + 4 >> 2]; - $2 = HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24); - HEAP8[$9 + 9 | 0] = $2; - HEAP8[$9 + 10 | 0] = $2 >>> 8; - HEAP8[$9 + 11 | 0] = $2 >>> 16; - HEAP8[$9 + 12 | 0] = $2 >>> 24; - HEAP8[$9 + 8 | 0] = $10; - HEAP8[$9 + 7 | 0] = $10 >>> 8; - $2 = HEAPU8[$1 + 34 | 0] | HEAPU8[$1 + 35 | 0] << 8 | (HEAPU8[$1 + 36 | 0] << 16 | HEAPU8[$1 + 37 | 0] << 24); - $10 = HEAPU8[$1 + 30 | 0] | HEAPU8[$1 + 31 | 0] << 8 | (HEAPU8[$1 + 32 | 0] << 16 | HEAPU8[$1 + 33 | 0] << 24); - HEAP8[$9 + 43 | 0] = $10; - HEAP8[$9 + 44 | 0] = $10 >>> 8; - HEAP8[$9 + 45 | 0] = $10 >>> 16; - HEAP8[$9 + 46 | 0] = $10 >>> 24; - HEAP8[$9 + 47 | 0] = $2; - HEAP8[$9 + 48 | 0] = $2 >>> 8; - HEAP8[$9 + 49 | 0] = $2 >>> 16; - HEAP8[$9 + 50 | 0] = $2 >>> 24; - $2 = HEAPU8[$1 + 28 | 0] | HEAPU8[$1 + 29 | 0] << 8 | (HEAPU8[$1 + 30 | 0] << 16 | HEAPU8[$1 + 31 | 0] << 24); - $10 = HEAPU8[$1 + 24 | 0] | HEAPU8[$1 + 25 | 0] << 8 | (HEAPU8[$1 + 26 | 0] << 16 | HEAPU8[$1 + 27 | 0] << 24); - HEAP8[$9 + 37 | 0] = $10; - HEAP8[$9 + 38 | 0] = $10 >>> 8; - HEAP8[$9 + 39 | 0] = $10 >>> 16; - HEAP8[$9 + 40 | 0] = $10 >>> 24; - HEAP8[$9 + 41 | 0] = $2; - HEAP8[$9 + 42 | 0] = $2 >>> 8; - HEAP8[$9 + 43 | 0] = $2 >>> 16; - HEAP8[$9 + 44 | 0] = $2 >>> 24; - $2 = HEAPU8[$1 + 20 | 0] | HEAPU8[$1 + 21 | 0] << 8 | (HEAPU8[$1 + 22 | 0] << 16 | HEAPU8[$1 + 23 | 0] << 24); - $10 = HEAPU8[$1 + 16 | 0] | HEAPU8[$1 + 17 | 0] << 8 | (HEAPU8[$1 + 18 | 0] << 16 | HEAPU8[$1 + 19 | 0] << 24); - HEAP8[$9 + 29 | 0] = $10; - HEAP8[$9 + 30 | 0] = $10 >>> 8; - HEAP8[$9 + 31 | 0] = $10 >>> 16; - HEAP8[$9 + 32 | 0] = $10 >>> 24; - HEAP8[$9 + 33 | 0] = $2; - HEAP8[$9 + 34 | 0] = $2 >>> 8; - HEAP8[$9 + 35 | 0] = $2 >>> 16; - HEAP8[$9 + 36 | 0] = $2 >>> 24; - $2 = HEAPU8[$1 + 12 | 0] | HEAPU8[$1 + 13 | 0] << 8 | (HEAPU8[$1 + 14 | 0] << 16 | HEAPU8[$1 + 15 | 0] << 24); - $10 = HEAPU8[$1 + 8 | 0] | HEAPU8[$1 + 9 | 0] << 8 | (HEAPU8[$1 + 10 | 0] << 16 | HEAPU8[$1 + 11 | 0] << 24); - HEAP8[$9 + 21 | 0] = $10; - HEAP8[$9 + 22 | 0] = $10 >>> 8; - HEAP8[$9 + 23 | 0] = $10 >>> 16; - HEAP8[$9 + 24 | 0] = $10 >>> 24; - HEAP8[$9 + 25 | 0] = $2; - HEAP8[$9 + 26 | 0] = $2 >>> 8; - HEAP8[$9 + 27 | 0] = $2 >>> 16; - HEAP8[$9 + 28 | 0] = $2 >>> 24; - $2 = HEAPU8[$1 + 4 | 0] | HEAPU8[$1 + 5 | 0] << 8 | (HEAPU8[$1 + 6 | 0] << 16 | HEAPU8[$1 + 7 | 0] << 24); - $1 = HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24); - HEAP8[$9 + 13 | 0] = $1; - HEAP8[$9 + 14 | 0] = $1 >>> 8; - HEAP8[$9 + 15 | 0] = $1 >>> 16; - HEAP8[$9 + 16 | 0] = $1 >>> 24; - HEAP8[$9 + 17 | 0] = $2; - HEAP8[$9 + 18 | 0] = $2 >>> 8; - HEAP8[$9 + 19 | 0] = $2 >>> 16; - HEAP8[$9 + 20 | 0] = $2 >>> 24; - HEAP32[$9 + 68 >> 2] = 51; - HEAP32[$9 + 72 >> 2] = 1; - HEAP32[$9 + 64 >> 2] = $9; - HEAP32[$0 + 388 >> 2] = 0; - break label$5; - } - HEAP32[$9 + 68 >> 2] = $2; - HEAP32[$9 + 64 >> 2] = $1; - } - if ($5) { - HEAP32[$9 + 76 >> 2] = 1 - } - $1 = $0 + 8 | 0; - if (ogg_stream_packetin($1, $9 - -64 | 0)) { - break label$4 - } - $2 = $0 + 368 | 0; - if (!$3) { - while (1) { - if (!ogg_stream_flush_i($1, $2, 1)) { - break label$2 - } - if (FUNCTION_TABLE[$6]($7, HEAP32[$0 + 368 >> 2], HEAP32[$0 + 372 >> 2], 0, $4, $8)) { - break label$4 - } - if (!FUNCTION_TABLE[$6]($7, HEAP32[$0 + 376 >> 2], HEAP32[$0 + 380 >> 2], 0, $4, $8)) { - continue - } - break label$4; - } - } - while (1) { - if (!ogg_stream_pageout($1, $2)) { - break label$2 - } - if (FUNCTION_TABLE[$6]($7, HEAP32[$0 + 368 >> 2], HEAP32[$0 + 372 >> 2], 0, $4, $8)) { - break label$4 - } - if (!FUNCTION_TABLE[$6]($7, HEAP32[$0 + 376 >> 2], HEAP32[$0 + 380 >> 2], 0, $4, $8)) { - continue - } - break; - }; - } - $6 = 1; - break label$1; - } - $6 = 1; - if ($3 | $4 | ($2 | 0) != 4 | (HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24)) != (HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24))) { - break label$1 - } - HEAP32[$0 + 384 >> 2] = 1; - $11 = $3; - } - $1 = $0; - $3 = $1; - $2 = HEAP32[$1 + 396 >> 2]; - $0 = $11 + HEAP32[$1 + 392 >> 2] | 0; - if ($0 >>> 0 < $11 >>> 0) { - $2 = $2 + 1 | 0 - } - HEAP32[$3 + 392 >> 2] = $0; - HEAP32[$1 + 396 >> 2] = $2; - $6 = 0; - } - global$0 = $9 + 96 | 0; - return $6; - } - - function simple_ogg_page__init($0) { - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - } - - function simple_ogg_page__clear($0) { - var $1 = 0; - $1 = HEAP32[$0 >> 2]; - if ($1) { - dlfree($1) - } - $1 = HEAP32[$0 + 8 >> 2]; - if ($1) { - dlfree($1) - } - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 0; - HEAP32[$0 + 12 >> 2] = 0; - } - - function simple_ogg_page__get_at($0, $1, $2, $3, $4, $5, $6) { - var $7 = 0, $8 = 0, $9 = 0; - $7 = global$0 - 16 | 0; - global$0 = $7; - label$1 : { - if (!$4) { - break label$1 - } - label$2 : { - switch (FUNCTION_TABLE[$4]($0, $1, $2, $6) | 0) { - case 1: - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$1; - case 0: - break label$2; - default: - break label$1; - }; - } - $4 = dlmalloc(282); - HEAP32[$3 >> 2] = $4; - if (!$4) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$1; - } - $8 = 27; - while (1) { - HEAP32[$7 + 12 >> 2] = $8; - $1 = 5; - label$6 : { - label$7 : { - switch (FUNCTION_TABLE[$5]($0, $4, $7 + 12 | 0, $6) | 0) { - case 1: - $1 = HEAP32[$7 + 12 >> 2]; - if ($1) { - break label$6 - } - $1 = 2; - default: - HEAP32[HEAP32[$0 >> 2] >> 2] = $1; - break label$1; - case 3: - break label$1; - case 0: - break label$7; - }; - } - $1 = HEAP32[$7 + 12 >> 2]; - } - $4 = $1 + $4 | 0; - $8 = $8 - $1 | 0; - if ($8) { - continue - } - break; - }; - $1 = HEAP32[$3 >> 2]; - HEAP32[$3 + 4 >> 2] = HEAPU8[$1 + 26 | 0] + 27; - label$10 : { - if (!(HEAP8[$1 + 5 | 0] & 1 | (HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24)) != 1399285583 | ((HEAPU8[$1 + 6 | 0] | HEAPU8[$1 + 7 | 0] << 8 | (HEAPU8[$1 + 8 | 0] << 16 | HEAPU8[$1 + 9 | 0] << 24)) != 0 | (HEAPU8[$1 + 10 | 0] | HEAPU8[$1 + 11 | 0] << 8 | (HEAPU8[$1 + 12 | 0] << 16 | HEAPU8[$1 + 13 | 0] << 24)) != 0))) { - $8 = HEAPU8[$1 + 26 | 0]; - if ($8) { - break label$10 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$1; - } - $4 = $1 + 27 | 0; - while (1) { - HEAP32[$7 + 12 >> 2] = $8; - $1 = 5; - label$13 : { - label$14 : { - switch (FUNCTION_TABLE[$5]($0, $4, $7 + 12 | 0, $6) | 0) { - case 1: - $1 = HEAP32[$7 + 12 >> 2]; - if ($1) { - break label$13 - } - $1 = 2; - default: - HEAP32[HEAP32[$0 >> 2] >> 2] = $1; - break label$1; - case 3: - break label$1; - case 0: - break label$14; - }; - } - $1 = HEAP32[$7 + 12 >> 2]; - } - $4 = $1 + $4 | 0; - $8 = $8 - $1 | 0; - if ($8) { - continue - } - break; - }; - $4 = 0; - $1 = HEAP32[$3 >> 2]; - $2 = HEAPU8[$1 + 26 | 0]; - label$17 : { - if (($2 | 0) != 1) { - $2 = $2 + -1 | 0; - while (1) { - if (HEAPU8[($1 + $4 | 0) + 27 | 0] != 255) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - break label$17; - } - $4 = $4 + 1 | 0; - if ($4 >>> 0 < $2 >>> 0) { - continue - } - break; - }; - } - $4 = HEAPU8[($1 + $4 | 0) + 27 | 0] + Math_imul($4, 255) | 0; - HEAP32[$3 + 12 >> 2] = $4; - $8 = dlmalloc($4 ? $4 : 1); - HEAP32[$3 + 8 >> 2] = $8; - if (!$8) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - break label$17; - } - $2 = $7; - if ($4) { - while (1) { - HEAP32[$7 + 12 >> 2] = $4; - $1 = 5; - label$24 : { - label$25 : { - switch (FUNCTION_TABLE[$5]($0, $8, $7 + 12 | 0, $6) | 0) { - case 1: - $1 = HEAP32[$7 + 12 >> 2]; - if ($1) { - break label$24 - } - $1 = 2; - default: - HEAP32[HEAP32[$0 >> 2] >> 2] = $1; - break label$17; - case 3: - break label$17; - case 0: - break label$25; - }; - } - $1 = HEAP32[$7 + 12 >> 2]; - } - $8 = $1 + $8 | 0; - $4 = $4 - $1 | 0; - if ($4) { - continue - } - break; - }; - $1 = HEAP32[$3 >> 2]; - } - HEAP32[$2 + 12 >> 2] = HEAPU8[$1 + 22 | 0] | HEAPU8[$1 + 23 | 0] << 8 | (HEAPU8[$1 + 24 | 0] << 16 | HEAPU8[$1 + 25 | 0] << 24); - ogg_page_checksum_set($3); - $1 = HEAP32[$3 >> 2]; - if (HEAP32[$7 + 12 >> 2] == (HEAPU8[$1 + 22 | 0] | HEAPU8[$1 + 23 | 0] << 8 | (HEAPU8[$1 + 24 | 0] << 16 | HEAPU8[$1 + 25 | 0] << 24))) { - $9 = 1; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - } - } - global$0 = $7 + 16 | 0; - return $9; - } - - function simple_ogg_page__set_at($0, $1, $2, $3, $4, $5, $6) { - folding_inner0 : { - label$1 : { - if (!$4) { - break label$1 - } - label$2 : { - switch (FUNCTION_TABLE[$4]($0, $1, $2, $6) | 0) { - case 1: - break folding_inner0; - case 0: - break label$2; - default: - break label$1; - }; - } - ogg_page_checksum_set($3); - if (FUNCTION_TABLE[$5]($0, HEAP32[$3 >> 2], HEAP32[$3 + 4 >> 2], 0, 0, $6)) { - break folding_inner0 - } - if (!FUNCTION_TABLE[$5]($0, HEAP32[$3 + 8 >> 2], HEAP32[$3 + 12 >> 2], 0, 0, $6)) { - return 1 - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - } - return 0; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - return 0; - } - - function __emscripten_stdout_close($0) { - $0 = $0 | 0; - return 0; - } - - function __emscripten_stdout_seek($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - i64toi32_i32$HIGH_BITS = 0; - return 0; - } - - function strcmp($0, $1) { - var $2 = 0, $3 = 0; - $2 = HEAPU8[$0 | 0]; - $3 = HEAPU8[$1 | 0]; - label$1 : { - if (!$2 | ($2 | 0) != ($3 | 0)) { - break label$1 - } - while (1) { - $3 = HEAPU8[$1 + 1 | 0]; - $2 = HEAPU8[$0 + 1 | 0]; - if (!$2) { - break label$1 - } - $1 = $1 + 1 | 0; - $0 = $0 + 1 | 0; - if (($2 | 0) == ($3 | 0)) { - continue - } - break; - }; - } - return $2 - $3 | 0; - } - - function __cos($0, $1) { - var $2 = 0.0, $3 = 0.0, $4 = 0.0, $5 = 0.0; - $2 = $0 * $0; - $3 = $2 * .5; - $4 = 1.0 - $3; - $5 = 1.0 - $4 - $3; - $3 = $2 * $2; - return $4 + ($5 + ($2 * ($2 * ($2 * ($2 * 2.480158728947673e-05 + -.001388888888887411) + .0416666666666666) + $3 * $3 * ($2 * ($2 * -1.1359647557788195e-11 + 2.087572321298175e-09) + -2.7557314351390663e-07)) - $0 * $1)); - } - - function scalbn($0, $1) { - label$1 : { - if (($1 | 0) >= 1024) { - $0 = $0 * 8988465674311579538646525.0e283; - if (($1 | 0) < 2047) { - $1 = $1 + -1023 | 0; - break label$1; - } - $0 = $0 * 8988465674311579538646525.0e283; - $1 = (($1 | 0) < 3069 ? $1 : 3069) + -2046 | 0; - break label$1; - } - if (($1 | 0) > -1023) { - break label$1 - } - $0 = $0 * 2.2250738585072014e-308; - if (($1 | 0) > -2045) { - $1 = $1 + 1022 | 0; - break label$1; - } - $0 = $0 * 2.2250738585072014e-308; - $1 = (($1 | 0) > -3066 ? $1 : -3066) + 2044 | 0; - } - wasm2js_scratch_store_i32(0, 0); - wasm2js_scratch_store_i32(1, $1 + 1023 << 20); - return $0 * +wasm2js_scratch_load_f64(); - } - - function __rem_pio2_large($0, $1, $2, $3) { - var $4 = 0.0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0.0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0; - $7 = global$0 - 560 | 0; - global$0 = $7; - $5 = ($2 + -3 | 0) / 24 | 0; - $16 = ($5 | 0) > 0 ? $5 : 0; - $10 = $2 + Math_imul($16, -24) | 0; - $12 = HEAP32[1901]; - $9 = $3 + -1 | 0; - if (($12 + $9 | 0) >= 0) { - $5 = $3 + $12 | 0; - $2 = $16 - $9 | 0; - while (1) { - HEAPF64[($7 + 320 | 0) + ($6 << 3) >> 3] = ($2 | 0) < 0 ? 0.0 : +HEAP32[($2 << 2) + 7616 >> 2]; - $2 = $2 + 1 | 0; - $6 = $6 + 1 | 0; - if (($5 | 0) != ($6 | 0)) { - continue - } - break; - }; - } - $13 = $10 + -24 | 0; - $5 = 0; - $6 = ($12 | 0) > 0 ? $12 : 0; - $11 = ($3 | 0) < 1; - while (1) { - label$6 : { - if ($11) { - $4 = 0.0; - break label$6; - } - $8 = $5 + $9 | 0; - $2 = 0; - $4 = 0.0; - while (1) { - $4 = $4 + HEAPF64[($2 << 3) + $0 >> 3] * HEAPF64[($7 + 320 | 0) + ($8 - $2 << 3) >> 3]; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - HEAPF64[($5 << 3) + $7 >> 3] = $4; - $2 = ($5 | 0) == ($6 | 0); - $5 = $5 + 1 | 0; - if (!$2) { - continue - } - break; - }; - $20 = 47 - $10 | 0; - $17 = 48 - $10 | 0; - $21 = $10 + -25 | 0; - $5 = $12; - label$9 : { - while (1) { - $4 = HEAPF64[($5 << 3) + $7 >> 3]; - $2 = 0; - $6 = $5; - $9 = ($5 | 0) < 1; - if (!$9) { - while (1) { - $11 = ($7 + 480 | 0) + ($2 << 2) | 0; - $14 = $4; - $4 = $4 * 5.9604644775390625e-08; - label$14 : { - if (Math_abs($4) < 2147483648.0) { - $8 = ~~$4; - break label$14; - } - $8 = -2147483648; - } - $4 = +($8 | 0); - $14 = $14 + $4 * -16777216.0; - label$13 : { - if (Math_abs($14) < 2147483648.0) { - $8 = ~~$14; - break label$13; - } - $8 = -2147483648; - } - HEAP32[$11 >> 2] = $8; - $6 = $6 + -1 | 0; - $4 = HEAPF64[($6 << 3) + $7 >> 3] + $4; - $2 = $2 + 1 | 0; - if (($5 | 0) != ($2 | 0)) { - continue - } - break; - } - } - $4 = scalbn($4, $13); - $4 = $4 + Math_floor($4 * .125) * -8.0; - label$17 : { - if (Math_abs($4) < 2147483648.0) { - $11 = ~~$4; - break label$17; - } - $11 = -2147483648; - } - $4 = $4 - +($11 | 0); - label$19 : { - label$20 : { - label$21 : { - $18 = ($13 | 0) < 1; - label$22 : { - if (!$18) { - $6 = (($5 << 2) + $7 | 0) + 476 | 0; - $8 = HEAP32[$6 >> 2]; - $2 = $8 >> $17; - $15 = $6; - $6 = $8 - ($2 << $17) | 0; - HEAP32[$15 >> 2] = $6; - $11 = $2 + $11 | 0; - $8 = $6 >> $20; - break label$22; - } - if ($13) { - break label$21 - } - $8 = HEAP32[(($5 << 2) + $7 | 0) + 476 >> 2] >> 23; - } - if (($8 | 0) < 1) { - break label$19 - } - break label$20; - } - $8 = 2; - if (!!($4 >= .5)) { - break label$20 - } - $8 = 0; - break label$19; - } - $2 = 0; - $6 = 0; - if (!$9) { - while (1) { - $15 = ($7 + 480 | 0) + ($2 << 2) | 0; - $19 = HEAP32[$15 >> 2]; - $9 = 16777215; - label$26 : { - label$27 : { - if ($6) { - break label$27 - } - $9 = 16777216; - if ($19) { - break label$27 - } - $6 = 0; - break label$26; - } - HEAP32[$15 >> 2] = $9 - $19; - $6 = 1; - } - $2 = $2 + 1 | 0; - if (($5 | 0) != ($2 | 0)) { - continue - } - break; - } - } - label$28 : { - if ($18) { - break label$28 - } - label$29 : { - switch ($21 | 0) { - case 0: - $2 = (($5 << 2) + $7 | 0) + 476 | 0; - HEAP32[$2 >> 2] = HEAP32[$2 >> 2] & 8388607; - break label$28; - case 1: - break label$29; - default: - break label$28; - }; - } - $2 = (($5 << 2) + $7 | 0) + 476 | 0; - HEAP32[$2 >> 2] = HEAP32[$2 >> 2] & 4194303; - } - $11 = $11 + 1 | 0; - if (($8 | 0) != 2) { - break label$19 - } - $4 = 1.0 - $4; - $8 = 2; - if (!$6) { - break label$19 - } - $4 = $4 - scalbn(1.0, $13); - } - if ($4 == 0.0) { - $6 = 0; - label$32 : { - $2 = $5; - if (($2 | 0) <= ($12 | 0)) { - break label$32 - } - while (1) { - $2 = $2 + -1 | 0; - $6 = HEAP32[($7 + 480 | 0) + ($2 << 2) >> 2] | $6; - if (($2 | 0) > ($12 | 0)) { - continue - } - break; - }; - if (!$6) { - break label$32 - } - $10 = $13; - while (1) { - $10 = $10 + -24 | 0; - $5 = $5 + -1 | 0; - if (!HEAP32[($7 + 480 | 0) + ($5 << 2) >> 2]) { - continue - } - break; - }; - break label$9; - } - $2 = 1; - while (1) { - $6 = $2; - $2 = $2 + 1 | 0; - if (!HEAP32[($7 + 480 | 0) + ($12 - $6 << 2) >> 2]) { - continue - } - break; - }; - $6 = $5 + $6 | 0; - while (1) { - $9 = $3 + $5 | 0; - $5 = $5 + 1 | 0; - HEAPF64[($7 + 320 | 0) + ($9 << 3) >> 3] = HEAP32[($16 + $5 << 2) + 7616 >> 2]; - $2 = 0; - $4 = 0.0; - if (($3 | 0) >= 1) { - while (1) { - $4 = $4 + HEAPF64[($2 << 3) + $0 >> 3] * HEAPF64[($7 + 320 | 0) + ($9 - $2 << 3) >> 3]; - $2 = $2 + 1 | 0; - if (($3 | 0) != ($2 | 0)) { - continue - } - break; - } - } - HEAPF64[($5 << 3) + $7 >> 3] = $4; - if (($5 | 0) < ($6 | 0)) { - continue - } - break; - }; - $5 = $6; - continue; - } - break; - }; - $4 = scalbn($4, 0 - $13 | 0); - label$39 : { - if (!!($4 >= 16777216.0)) { - $3 = ($7 + 480 | 0) + ($5 << 2) | 0; - $14 = $4; - $4 = $4 * 5.9604644775390625e-08; - label$42 : { - if (Math_abs($4) < 2147483648.0) { - $2 = ~~$4; - break label$42; - } - $2 = -2147483648; - } - $4 = $14 + +($2 | 0) * -16777216.0; - label$41 : { - if (Math_abs($4) < 2147483648.0) { - $0 = ~~$4; - break label$41; - } - $0 = -2147483648; - } - HEAP32[$3 >> 2] = $0; - $5 = $5 + 1 | 0; - break label$39; - } - $2 = Math_abs($4) < 2147483648.0 ? ~~$4 : -2147483648; - $10 = $13; - } - HEAP32[($7 + 480 | 0) + ($5 << 2) >> 2] = $2; - } - $4 = scalbn(1.0, $10); - label$47 : { - if (($5 | 0) <= -1) { - break label$47 - } - $2 = $5; - while (1) { - HEAPF64[($2 << 3) + $7 >> 3] = $4 * +HEAP32[($7 + 480 | 0) + ($2 << 2) >> 2]; - $4 = $4 * 5.9604644775390625e-08; - $0 = ($2 | 0) > 0; - $2 = $2 + -1 | 0; - if ($0) { - continue - } - break; - }; - $9 = 0; - if (($5 | 0) < 0) { - break label$47 - } - $0 = ($12 | 0) > 0 ? $12 : 0; - $6 = $5; - while (1) { - $3 = $0 >>> 0 < $9 >>> 0 ? $0 : $9; - $10 = $5 - $6 | 0; - $2 = 0; - $4 = 0.0; - while (1) { - $4 = $4 + HEAPF64[($2 << 3) + 10384 >> 3] * HEAPF64[($2 + $6 << 3) + $7 >> 3]; - $13 = ($2 | 0) != ($3 | 0); - $2 = $2 + 1 | 0; - if ($13) { - continue - } - break; - }; - HEAPF64[($7 + 160 | 0) + ($10 << 3) >> 3] = $4; - $6 = $6 + -1 | 0; - $2 = ($5 | 0) != ($9 | 0); - $9 = $9 + 1 | 0; - if ($2) { - continue - } - break; - }; - } - $4 = 0.0; - if (($5 | 0) >= 0) { - $2 = $5; - while (1) { - $4 = $4 + HEAPF64[($7 + 160 | 0) + ($2 << 3) >> 3]; - $0 = ($2 | 0) > 0; - $2 = $2 + -1 | 0; - if ($0) { - continue - } - break; - }; - } - HEAPF64[$1 >> 3] = $8 ? -$4 : $4; - $4 = HEAPF64[$7 + 160 >> 3] - $4; - $2 = 1; - if (($5 | 0) >= 1) { - while (1) { - $4 = $4 + HEAPF64[($7 + 160 | 0) + ($2 << 3) >> 3]; - $0 = ($2 | 0) != ($5 | 0); - $2 = $2 + 1 | 0; - if ($0) { - continue - } - break; - } - } - HEAPF64[$1 + 8 >> 3] = $8 ? -$4 : $4; - global$0 = $7 + 560 | 0; - return $11 & 7; - } - - function __rem_pio2($0, $1) { - var $2 = 0.0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0.0, $9 = 0.0, $10 = 0; - $6 = global$0 - 48 | 0; - global$0 = $6; - wasm2js_scratch_store_f64(+$0); - $5 = wasm2js_scratch_load_i32(1) | 0; - $3 = wasm2js_scratch_load_i32(0) | 0; - label$1 : { - label$2 : { - $4 = $5; - $5 = $4; - $7 = $4 & 2147483647; - label$3 : { - if ($7 >>> 0 <= 1074752122) { - if (($5 & 1048575) == 598523) { - break label$3 - } - if ($7 >>> 0 <= 1073928572) { - if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { - $0 = $0 + -1.5707963267341256; - $2 = $0 + -6.077100506506192e-11; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + -6.077100506506192e-11; - $3 = 1; - break label$1; - } - $0 = $0 + 1.5707963267341256; - $2 = $0 + 6.077100506506192e-11; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + 6.077100506506192e-11; - $3 = -1; - break label$1; - } - if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { - $0 = $0 + -3.1415926534682512; - $2 = $0 + -1.2154201013012384e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + -1.2154201013012384e-10; - $3 = 2; - break label$1; - } - $0 = $0 + 3.1415926534682512; - $2 = $0 + 1.2154201013012384e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + 1.2154201013012384e-10; - $3 = -2; - break label$1; - } - if ($7 >>> 0 <= 1075594811) { - if ($7 >>> 0 <= 1075183036) { - if (($7 | 0) == 1074977148) { - break label$3 - } - if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { - $0 = $0 + -4.712388980202377; - $2 = $0 + -1.8231301519518578e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + -1.8231301519518578e-10; - $3 = 3; - break label$1; - } - $0 = $0 + 4.712388980202377; - $2 = $0 + 1.8231301519518578e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + 1.8231301519518578e-10; - $3 = -3; - break label$1; - } - if (($7 | 0) == 1075388923) { - break label$3 - } - if (($4 | 0) > 0 ? 1 : ($4 | 0) >= 0 ? ($3 >>> 0 < 0 ? 0 : 1) : 0) { - $0 = $0 + -6.2831853069365025; - $2 = $0 + -2.430840202602477e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + -2.430840202602477e-10; - $3 = 4; - break label$1; - } - $0 = $0 + 6.2831853069365025; - $2 = $0 + 2.430840202602477e-10; - HEAPF64[$1 >> 3] = $2; - HEAPF64[$1 + 8 >> 3] = $0 - $2 + 2.430840202602477e-10; - $3 = -4; - break label$1; - } - if ($7 >>> 0 > 1094263290) { - break label$2 - } - } - $9 = $0 * .6366197723675814 + 6755399441055744.0 + -6755399441055744.0; - $2 = $0 + $9 * -1.5707963267341256; - $8 = $9 * 6.077100506506192e-11; - $0 = $2 - $8; - HEAPF64[$1 >> 3] = $0; - wasm2js_scratch_store_f64(+$0); - $3 = wasm2js_scratch_load_i32(1) | 0; - wasm2js_scratch_load_i32(0) | 0; - $4 = $7 >>> 20 | 0; - $5 = ($4 - ($3 >>> 20 & 2047) | 0) < 17; - if (Math_abs($9) < 2147483648.0) { - $3 = ~~$9 - } else { - $3 = -2147483648 - } - label$14 : { - if ($5) { - break label$14 - } - $8 = $2; - $0 = $9 * 6.077100506303966e-11; - $2 = $2 - $0; - $8 = $9 * 2.0222662487959506e-21 - ($8 - $2 - $0); - $0 = $2 - $8; - HEAPF64[$1 >> 3] = $0; - $5 = $4; - wasm2js_scratch_store_f64(+$0); - $4 = wasm2js_scratch_load_i32(1) | 0; - wasm2js_scratch_load_i32(0) | 0; - if (($5 - ($4 >>> 20 & 2047) | 0) < 50) { - break label$14 - } - $8 = $2; - $0 = $9 * 2.0222662487111665e-21; - $2 = $2 - $0; - $8 = $9 * 8.4784276603689e-32 - ($8 - $2 - $0); - $0 = $2 - $8; - HEAPF64[$1 >> 3] = $0; - } - HEAPF64[$1 + 8 >> 3] = $2 - $0 - $8; - break label$1; - } - if ($7 >>> 0 >= 2146435072) { - $0 = $0 - $0; - HEAPF64[$1 >> 3] = $0; - HEAPF64[$1 + 8 >> 3] = $0; - $3 = 0; - break label$1; - } - wasm2js_scratch_store_i32(0, $3 | 0); - wasm2js_scratch_store_i32(1, $4 & 1048575 | 1096810496); - $0 = +wasm2js_scratch_load_f64(); - $3 = 0; - $5 = 1; - while (1) { - $10 = ($6 + 16 | 0) + ($3 << 3) | 0; - if (Math_abs($0) < 2147483648.0) { - $3 = ~~$0 - } else { - $3 = -2147483648 - } - $2 = +($3 | 0); - HEAPF64[$10 >> 3] = $2; - $0 = ($0 - $2) * 16777216.0; - $3 = 1; - $10 = $5 & 1; - $5 = 0; - if ($10) { - continue - } - break; - }; - HEAPF64[$6 + 32 >> 3] = $0; - label$20 : { - if ($0 != 0.0) { - $3 = 2; - break label$20; - } - $5 = 1; - while (1) { - $3 = $5; - $5 = $3 + -1 | 0; - if (HEAPF64[($6 + 16 | 0) + ($3 << 3) >> 3] == 0.0) { - continue - } - break; - }; - } - $3 = __rem_pio2_large($6 + 16 | 0, $6, ($7 >>> 20 | 0) + -1046 | 0, $3 + 1 | 0); - $0 = HEAPF64[$6 >> 3]; - if (($4 | 0) < -1 ? 1 : ($4 | 0) <= -1 ? 1 : 0) { - HEAPF64[$1 >> 3] = -$0; - HEAPF64[$1 + 8 >> 3] = -HEAPF64[$6 + 8 >> 3]; - $3 = 0 - $3 | 0; - break label$1; - } - HEAPF64[$1 >> 3] = $0; - $4 = HEAP32[$6 + 12 >> 2]; - HEAP32[$1 + 8 >> 2] = HEAP32[$6 + 8 >> 2]; - HEAP32[$1 + 12 >> 2] = $4; - } - global$0 = $6 + 48 | 0; - return $3; - } - - function __sin($0, $1) { - var $2 = 0.0, $3 = 0.0; - $2 = $0 * $0; - $3 = $0; - $0 = $2 * $0; - return $3 - ($2 * ($1 * .5 - $0 * ($2 * ($2 * $2) * ($2 * 1.58969099521155e-10 + -2.5050760253406863e-08) + ($2 * ($2 * 2.7557313707070068e-06 + -1.984126982985795e-04) + .00833333333332249))) - $1 + $0 * .16666666666666632); - } - - function cos($0) { - var $1 = 0, $2 = 0.0, $3 = 0; - $1 = global$0 - 16 | 0; - global$0 = $1; - wasm2js_scratch_store_f64(+$0); - $3 = wasm2js_scratch_load_i32(1) | 0; - wasm2js_scratch_load_i32(0) | 0; - $3 = $3 & 2147483647; - label$1 : { - if ($3 >>> 0 <= 1072243195) { - $2 = 1.0; - if ($3 >>> 0 < 1044816030) { - break label$1 - } - $2 = __cos($0, 0.0); - break label$1; - } - $2 = $0 - $0; - if ($3 >>> 0 >= 2146435072) { - break label$1 - } - label$3 : { - switch (__rem_pio2($0, $1) & 3) { - case 0: - $2 = __cos(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); - break label$1; - case 1: - $2 = -__sin(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); - break label$1; - case 2: - $2 = -__cos(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); - break label$1; - default: - break label$3; - }; - } - $2 = __sin(HEAPF64[$1 >> 3], HEAPF64[$1 + 8 >> 3]); - } - $0 = $2; - global$0 = $1 + 16 | 0; - return $0; - } - - function exp($0) { - var $1 = 0, $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0, $6 = 0.0, $7 = 0; - wasm2js_scratch_store_f64(+$0); - $3 = wasm2js_scratch_load_i32(1) | 0; - $7 = wasm2js_scratch_load_i32(0) | 0; - $5 = $3 >>> 31 | 0; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - $6 = $0; - label$5 : { - label$6 : { - $1 = $3; - $3 = $1 & 2147483647; - label$7 : { - if ($3 >>> 0 >= 1082532651) { - $1 = $1 & 2147483647; - if (($1 | 0) == 2146435072 & $7 >>> 0 > 0 | $1 >>> 0 > 2146435072) { - return $0 - } - if (!!($0 > 709.782712893384)) { - return $0 * 8988465674311579538646525.0e283 - } - if (!($0 < -708.3964185322641)) { - break label$7 - } - if (!($0 < -745.1332191019411)) { - break label$7 - } - break label$2; - } - if ($3 >>> 0 < 1071001155) { - break label$4 - } - if ($3 >>> 0 < 1072734898) { - break label$6 - } - } - $0 = $0 * 1.4426950408889634 + HEAPF64[($5 << 3) + 10448 >> 3]; - if (Math_abs($0) < 2147483648.0) { - $1 = ~~$0; - break label$5; - } - $1 = -2147483648; - break label$5; - } - $1 = ($5 ^ 1) - $5 | 0; - } - $2 = +($1 | 0); - $0 = $6 + $2 * -.6931471803691238; - $4 = $2 * 1.9082149292705877e-10; - $2 = $0 - $4; - break label$3; - } - if ($3 >>> 0 <= 1043333120) { - break label$1 - } - $1 = 0; - $2 = $0; - } - $6 = $0; - $0 = $2 * $2; - $0 = $2 - $0 * ($0 * ($0 * ($0 * ($0 * 4.1381367970572385e-08 + -1.6533902205465252e-06) + 6.613756321437934e-05) + -2.7777777777015593e-03) + .16666666666666602); - $4 = $6 + ($2 * $0 / (2.0 - $0) - $4) + 1.0; - if (!$1) { - break label$2 - } - $4 = scalbn($4, $1); - } - return $4; - } - return $0 + 1.0; - } - - function FLAC__window_bartlett($0, $1) { - var $2 = 0, $3 = Math_fround(0), $4 = 0, $5 = Math_fround(0), $6 = 0, $7 = 0, $8 = 0; - $7 = $1 + -1 | 0; - label$1 : { - if ($1 & 1) { - $4 = ($7 | 0) / 2 | 0; - if (($1 | 0) >= 0) { - $8 = ($4 | 0) > 0 ? $4 : 0; - $6 = $8 + 1 | 0; - $5 = Math_fround($7 | 0); - while (1) { - $3 = Math_fround($2 | 0); - HEAPF32[($2 << 2) + $0 >> 2] = Math_fround($3 + $3) / $5; - $4 = ($2 | 0) == ($8 | 0); - $2 = $2 + 1 | 0; - if (!$4) { - continue - } - break; - }; - } - if (($6 | 0) >= ($1 | 0)) { - break label$1 - } - $5 = Math_fround($7 | 0); - while (1) { - $3 = Math_fround($6 | 0); - HEAPF32[($6 << 2) + $0 >> 2] = Math_fround(2.0) - Math_fround(Math_fround($3 + $3) / $5); - $6 = $6 + 1 | 0; - if (($6 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - $4 = ($1 | 0) / 2 | 0; - if (($1 | 0) >= 2) { - $5 = Math_fround($7 | 0); - while (1) { - $3 = Math_fround($2 | 0); - HEAPF32[($2 << 2) + $0 >> 2] = Math_fround($3 + $3) / $5; - $2 = $2 + 1 | 0; - if (($4 | 0) != ($2 | 0)) { - continue - } - break; - }; - $2 = $4; - } - if (($2 | 0) >= ($1 | 0)) { - break label$1 - } - $5 = Math_fround($7 | 0); - while (1) { - $3 = Math_fround($2 | 0); - HEAPF32[($2 << 2) + $0 >> 2] = Math_fround(2.0) - Math_fround(Math_fround($3 + $3) / $5); - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_bartlett_hann($0, $1) { - var $2 = 0, $3 = Math_fround(0), $4 = Math_fround(0), wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $4 = Math_fround($1 + -1 | 0); - while (1) { - $3 = Math_fround(Math_fround($2 | 0) / $4); - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(+Math_fround(Math_abs(Math_fround($3 + Math_fround(-.5)))) * -.47999998927116394 + .6200000047683716 + cos(+$3 * 6.283185307179586) * -.3799999952316284)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_blackman($0, $1) { - var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - $4 = +($2 | 0); - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .07999999821186066 + (cos($4 * 6.283185307179586 / $3) * -.5 + .41999998688697815))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_blackman_harris_4term_92db_sidelobe($0, $1) { - var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - $4 = +($2 | 0); - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .14127999544143677 + (cos($4 * 6.283185307179586 / $3) * -.488290011882782 + .35874998569488525) + cos($4 * 18.84955592153876 / $3) * -.011680000461637974)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_connes($0, $1) { - var $2 = 0.0, $3 = 0, $4 = 0.0; - if (($1 | 0) >= 1) { - $4 = +($1 + -1 | 0) * .5; - while (1) { - $2 = (+($3 | 0) - $4) / $4; - $2 = 1.0 - $2 * $2; - HEAPF32[($3 << 2) + $0 >> 2] = $2 * $2; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_flattop($0, $1) { - var $2 = 0.0, $3 = 0, $4 = 0.0, $5 = 0.0, $6 = 0.0, $7 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $2 = +($1 + -1 | 0); - while (1) { - $4 = +($3 | 0); - $5 = cos($4 * 12.566370614359172 / $2); - $6 = cos($4 * 6.283185307179586 / $2); - $7 = cos($4 * 18.84955592153876 / $2); - (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 25.132741228718345 / $2) * 6.9473679177463055e-03 + ($5 * .27726316452026367 + ($6 * -.4166315793991089 + .21557894349098206) + $7 * -.08357894420623779))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_gauss($0, $1, $2) { - var $3 = 0, $4 = 0.0, $5 = 0.0, $6 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $4 = +($1 + -1 | 0) * .5; - $6 = $4 * +$2; - while (1) { - $5 = (+($3 | 0) - $4) / $6; - (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(exp($5 * ($5 * -.5)))), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_hamming($0, $1) { - var $2 = 0, $3 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos(+($2 | 0) * 6.283185307179586 / $3) * -.46000000834465027 + .5400000214576721)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_hann($0, $1) { - var $2 = 0, $3 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($2 | 0) * 6.283185307179586 / $3) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_kaiser_bessel($0, $1) { - var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - $4 = +($2 | 0); - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .09799999743700027 + (cos($4 * 6.283185307179586 / $3) * -.49799999594688416 + .4020000100135803) + cos($4 * 18.84955592153876 / $3) * -1.0000000474974513e-03)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_nuttall($0, $1) { - var $2 = 0, $3 = 0.0, $4 = 0.0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0); - while (1) { - $4 = +($2 | 0); - (wasm2js_i32$0 = ($2 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(cos($4 * 12.566370614359172 / $3) * .13659949600696564 + (cos($4 * 6.283185307179586 / $3) * -.48917749524116516 + .36358189582824707) + cos($4 * 18.84955592153876 / $3) * -.010641099885106087)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_rectangle($0, $1) { - var $2 = 0; - if (($1 | 0) >= 1) { - while (1) { - HEAP32[($2 << 2) + $0 >> 2] = 1065353216; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - } - } - } - - function FLAC__window_triangle($0, $1) { - var $2 = 0, $3 = 0, $4 = Math_fround(0), $5 = 0, $6 = Math_fround(0), $7 = 0; - $3 = 1; - label$1 : { - if ($1 & 1) { - $2 = ($1 + 1 | 0) / 2 | 0; - if (($1 | 0) >= 1) { - $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); - $5 = ($2 | 0) > 1 ? $2 : 1; - $3 = $5 + 1 | 0; - $2 = 1; - while (1) { - $6 = Math_fround($2 | 0); - HEAPF32[(($2 << 2) + $0 | 0) + -4 >> 2] = Math_fround($6 + $6) / $4; - $7 = ($2 | 0) == ($5 | 0); - $2 = $2 + 1 | 0; - if (!$7) { - continue - } - break; - }; - } - if (($3 | 0) > ($1 | 0)) { - break label$1 - } - $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); - while (1) { - HEAPF32[(($3 << 2) + $0 | 0) + -4 >> 2] = Math_fround(($1 - $3 << 1) + 2 | 0) / $4; - $2 = ($1 | 0) == ($3 | 0); - $3 = $3 + 1 | 0; - if (!$2) { - continue - } - break; - }; - break label$1; - } - $2 = 1; - if (($1 | 0) >= 2) { - $5 = $1 >>> 1 | 0; - $2 = $5 + 1 | 0; - $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); - while (1) { - $6 = Math_fround($3 | 0); - HEAPF32[(($3 << 2) + $0 | 0) + -4 >> 2] = Math_fround($6 + $6) / $4; - $7 = ($3 | 0) == ($5 | 0); - $3 = $3 + 1 | 0; - if (!$7) { - continue - } - break; - }; - } - if (($2 | 0) > ($1 | 0)) { - break label$1 - } - $4 = Math_fround(Math_fround($1 | 0) + Math_fround(1.0)); - while (1) { - HEAPF32[(($2 << 2) + $0 | 0) + -4 >> 2] = Math_fround(($1 - $2 << 1) + 2 | 0) / $4; - $3 = ($1 | 0) != ($2 | 0); - $2 = $2 + 1 | 0; - if ($3) { - continue - } - break; - }; - } - } - - function FLAC__window_tukey($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0.0, $6 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - label$1 : { - if (!!($2 <= Math_fround(0.0))) { - if (($1 | 0) < 1) { - break label$1 - } - while (1) { - HEAP32[($3 << 2) + $0 >> 2] = 1065353216; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - if (!!($2 >= Math_fround(1.0))) { - if (($1 | 0) < 1) { - break label$1 - } - $5 = +($1 + -1 | 0); - while (1) { - (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 | 0) * 6.283185307179586 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - break label$1; - } - $2 = Math_fround(Math_fround($2 * Math_fround(.5)) * Math_fround($1 | 0)); - label$6 : { - if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { - $4 = ~~$2; - break label$6; - } - $4 = -2147483648; - } - if (($1 | 0) >= 1) { - while (1) { - HEAP32[($3 << 2) + $0 >> 2] = 1065353216; - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - } - } - if (($4 | 0) < 2) { - break label$1 - } - $1 = $1 - $4 | 0; - $6 = $4 + -1 | 0; - $5 = +($6 | 0); - $3 = 0; - while (1) { - (wasm2js_i32$0 = ($3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 | 0) * 3.141592653589793 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - (wasm2js_i32$0 = ($1 + $3 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($3 + $6 | 0) * 3.141592653589793 / $5) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $3 = $3 + 1 | 0; - if (($4 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_partial_tukey($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = Math_fround(0), $12 = 0.0, $13 = 0, wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - while (1) { - $11 = $2; - $2 = Math_fround(.05000000074505806); - if ($11 <= Math_fround(0.0)) { - continue - } - $2 = Math_fround(.949999988079071); - if ($11 >= Math_fround(1.0)) { - continue - } - break; - }; - $2 = Math_fround($1 | 0); - $3 = Math_fround($2 * $3); - label$2 : { - if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { - $6 = ~~$3; - break label$2; - } - $6 = -2147483648; - } - $3 = Math_fround($11 * Math_fround(.5)); - $2 = Math_fround($2 * $4); - label$5 : { - if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { - $10 = ~~$2; - break label$5; - } - $10 = -2147483648; - } - $2 = Math_fround($3 * Math_fround($10 - $6 | 0)); - label$4 : { - if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { - $7 = ~~$2; - break label$4; - } - $7 = -2147483648; - } - if (!(($6 | 0) < 1 | ($1 | 0) < 1)) { - $5 = $6 + -1 | 0; - $8 = $1 + -1 | 0; - $8 = $5 >>> 0 < $8 >>> 0 ? $5 : $8; - memset($0, ($8 << 2) + 4 | 0); - $5 = $8 + 1 | 0; - while (1) { - $13 = ($9 | 0) == ($8 | 0); - $9 = $9 + 1 | 0; - if (!$13) { - continue - } - break; - }; - } - $6 = $6 + $7 | 0; - label$10 : { - if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$10 - } - $12 = +($7 | 0); - $9 = 1; - while (1) { - (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($9 | 0) * 3.141592653589793 / $12) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($6 | 0)) { - break label$10 - } - $9 = $9 + 1 | 0; - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - $6 = $10 - $7 | 0; - label$12 : { - if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$12 - } - while (1) { - HEAP32[($5 << 2) + $0 >> 2] = 1065353216; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($6 | 0)) { - break label$12 - } - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - label$14 : { - if (($5 | 0) >= ($10 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$14 - } - $12 = +($7 | 0); - while (1) { - (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($7 | 0) * 3.141592653589793 / $12) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($10 | 0)) { - break label$14 - } - $7 = $7 + -1 | 0; - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - if (($5 | 0) < ($1 | 0)) { - memset(($5 << 2) + $0 | 0, $1 - $5 << 2) - } - } - - function FLAC__window_punchout_tukey($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0.0, $12 = Math_fround(0), $13 = 0, $14 = Math_fround(0), wasm2js_i32$0 = 0, wasm2js_f32$0 = Math_fround(0); - while (1) { - $12 = $2; - $2 = Math_fround(.05000000074505806); - if ($12 <= Math_fround(0.0)) { - continue - } - $2 = Math_fround(.949999988079071); - if ($12 >= Math_fround(1.0)) { - continue - } - break; - }; - $2 = Math_fround($12 * Math_fround(.5)); - $14 = $2; - $12 = Math_fround($1 | 0); - $3 = Math_fround($12 * $3); - label$3 : { - if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { - $10 = ~~$3; - break label$3; - } - $10 = -2147483648; - } - $3 = Math_fround($14 * Math_fround($10 | 0)); - label$2 : { - if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { - $6 = ~~$3; - break label$2; - } - $6 = -2147483648; - } - $8 = ($6 | 0) < 1; - $7 = $1; - $3 = Math_fround($12 * $4); - label$7 : { - if (Math_fround(Math_abs($3)) < Math_fround(2147483648.0)) { - $9 = ~~$3; - break label$7; - } - $9 = -2147483648; - } - $2 = Math_fround($2 * Math_fround($7 - $9 | 0)); - label$6 : { - if (Math_fround(Math_abs($2)) < Math_fround(2147483648.0)) { - $7 = ~~$2; - break label$6; - } - $7 = -2147483648; - } - if (!(($1 | 0) < 1 | $8)) { - $5 = $6 + -1 >>> 0 < $1 + -1 >>> 0 ? $6 : $1; - $11 = +($6 | 0); - $8 = 0; - $13 = 1; - while (1) { - (wasm2js_i32$0 = ($8 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($13 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $13 = $13 + 1 | 0; - $8 = $8 + 1 | 0; - if (($8 | 0) != ($5 | 0)) { - continue - } - break; - }; - } - $8 = $10 - $6 | 0; - label$12 : { - if (($5 | 0) >= ($8 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$12 - } - while (1) { - HEAP32[($5 << 2) + $0 >> 2] = 1065353216; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($8 | 0)) { - break label$12 - } - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - label$14 : { - if (($5 | 0) >= ($10 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$14 - } - $11 = +($6 | 0); - while (1) { - (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($6 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($10 | 0)) { - break label$14 - } - $6 = $6 + -1 | 0; - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - label$16 : { - if (($5 | 0) >= ($9 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$16 - } - $6 = $5 ^ -1; - $10 = $6 + $9 | 0; - $6 = $1 + $6 | 0; - memset(($5 << 2) + $0 | 0, (($10 >>> 0 < $6 >>> 0 ? $10 : $6) << 2) + 4 | 0); - while (1) { - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($9 | 0)) { - break label$16 - } - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - $9 = $7 + $9 | 0; - label$18 : { - if (($5 | 0) >= ($9 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$18 - } - $11 = +($7 | 0); - $6 = 1; - while (1) { - (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($6 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($9 | 0)) { - break label$18 - } - $6 = $6 + 1 | 0; - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - $6 = $1 - $7 | 0; - label$20 : { - if (($5 | 0) >= ($6 | 0) | ($5 | 0) >= ($1 | 0)) { - break label$20 - } - while (1) { - HEAP32[($5 << 2) + $0 >> 2] = 1065353216; - $5 = $5 + 1 | 0; - if (($5 | 0) >= ($6 | 0)) { - break label$20 - } - if (($5 | 0) < ($1 | 0)) { - continue - } - break; - }; - } - if (($5 | 0) < ($1 | 0)) { - $11 = +($7 | 0); - while (1) { - (wasm2js_i32$0 = ($5 << 2) + $0 | 0, wasm2js_f32$0 = Math_fround(.5 - cos(+($7 | 0) * 3.141592653589793 / $11) * .5)), HEAPF32[wasm2js_i32$0 >> 2] = wasm2js_f32$0; - $7 = $7 + -1 | 0; - $5 = $5 + 1 | 0; - if (($5 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__window_welch($0, $1) { - var $2 = 0, $3 = 0.0, $4 = 0.0; - if (($1 | 0) >= 1) { - $3 = +($1 + -1 | 0) * .5; - while (1) { - $4 = (+($2 | 0) - $3) / $3; - HEAPF32[($2 << 2) + $0 >> 2] = 1.0 - $4 * $4; - $2 = $2 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - } - - function FLAC__add_metadata_block($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0; - $3 = strlen(HEAP32[2720]); - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 4 >> 2], HEAP32[1391])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 >> 2], HEAP32[1392])) { - break label$1 - } - $2 = HEAP32[$0 + 8 >> 2]; - $2 = HEAP32[$0 >> 2] == 4 ? ($2 + $3 | 0) - HEAP32[$0 + 16 >> 2] | 0 : $2; - $4 = HEAP32[1393]; - if ($2 >>> $4) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, $2, $4)) { - break label$1 - } - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - label$8 : { - label$9 : { - switch (HEAP32[$0 >> 2]) { - case 3: - if (!HEAP32[$0 + 16 >> 2]) { - break label$3 - } - $4 = HEAP32[1367]; - $6 = HEAP32[1366]; - $7 = HEAP32[1365]; - $2 = 0; - break label$8; - case 0: - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 16 >> 2], HEAP32[1356])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 20 >> 2], HEAP32[1357])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 24 >> 2], HEAP32[1358])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 28 >> 2], HEAP32[1359])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 32 >> 2], HEAP32[1360])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 36 >> 2] + -1 | 0, HEAP32[1361])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 40 >> 2] + -1 | 0, HEAP32[1362])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$0 + 48 >> 2], HEAP32[$0 + 52 >> 2], HEAP32[1363])) { - break label$1 - } - if (FLAC__bitwriter_write_byte_block($1, $0 + 56 | 0, 16)) { - break label$3 - } - break label$1; - case 1: - if (FLAC__bitwriter_write_zeroes($1, HEAP32[$0 + 8 >> 2] << 3)) { - break label$3 - } - break label$1; - case 6: - break label$5; - case 5: - break label$6; - case 4: - break label$7; - case 2: - break label$9; - default: - break label$4; - }; - } - $2 = HEAP32[1364] >>> 3 | 0; - if (!FLAC__bitwriter_write_byte_block($1, $0 + 16 | 0, $2)) { - break label$1 - } - if (FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 20 >> 2], HEAP32[$0 + 8 >> 2] - $2 | 0)) { - break label$3 - } - break label$1; - } - while (1) { - $3 = Math_imul($2, 24); - $5 = $3 + HEAP32[$0 + 20 >> 2] | 0; - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$5 >> 2], HEAP32[$5 + 4 >> 2], $7)) { - break label$1 - } - $5 = $3 + HEAP32[$0 + 20 >> 2] | 0; - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$5 + 8 >> 2], HEAP32[$5 + 12 >> 2], $6)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[($3 + HEAP32[$0 + 20 >> 2] | 0) + 16 >> 2], $4)) { - break label$1 - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 < HEAPU32[$0 + 16 >> 2]) { - continue - } - break; - }; - break label$3; - } - if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, $3)) { - break label$1 - } - if (!FLAC__bitwriter_write_byte_block($1, HEAP32[2720], $3)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, HEAP32[$0 + 24 >> 2])) { - break label$1 - } - if (!HEAP32[$0 + 24 >> 2]) { - break label$3 - } - $2 = 0; - while (1) { - $3 = $2 << 3; - if (!FLAC__bitwriter_write_raw_uint32_little_endian($1, HEAP32[$3 + HEAP32[$0 + 28 >> 2] >> 2])) { - break label$1 - } - $3 = $3 + HEAP32[$0 + 28 >> 2] | 0; - if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$3 + 4 >> 2], HEAP32[$3 >> 2])) { - break label$1 - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 < HEAPU32[$0 + 24 >> 2]) { - continue - } - break; - }; - break label$3; - } - if (!FLAC__bitwriter_write_byte_block($1, $0 + 16 | 0, HEAP32[1378] >>> 3 | 0)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$0 + 152 >> 2], HEAP32[$0 + 156 >> 2], HEAP32[1379])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 160 >> 2] != 0, HEAP32[1380])) { - break label$1 - } - if (!FLAC__bitwriter_write_zeroes($1, HEAP32[1381])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 164 >> 2], HEAP32[1382])) { - break label$1 - } - if (!HEAP32[$0 + 164 >> 2]) { - break label$3 - } - $6 = HEAP32[1373] >>> 3 | 0; - $7 = HEAP32[1370]; - $5 = HEAP32[1369]; - $9 = HEAP32[1368]; - $10 = HEAP32[1377]; - $11 = HEAP32[1376]; - $12 = HEAP32[1375]; - $13 = HEAP32[1374]; - $14 = HEAP32[1372]; - $15 = HEAP32[1371]; - $3 = 0; - while (1) { - $2 = HEAP32[$0 + 168 >> 2] + ($3 << 5) | 0; - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$2 >> 2], HEAP32[$2 + 4 >> 2], $15)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 8 | 0], $14)) { - break label$1 - } - if (!FLAC__bitwriter_write_byte_block($1, $2 + 9 | 0, $6)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP8[$2 + 22 | 0] & 1, $13)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 22 | 0] >>> 1 & 1, $12)) { - break label$1 - } - if (!FLAC__bitwriter_write_zeroes($1, $11)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$2 + 23 | 0], $10)) { - break label$1 - } - label$16 : { - $8 = $2 + 23 | 0; - if (!HEAPU8[$8 | 0]) { - break label$16 - } - $16 = $2 + 24 | 0; - $2 = 0; - while (1) { - $4 = HEAP32[$16 >> 2] + ($2 << 4) | 0; - if (!FLAC__bitwriter_write_raw_uint64($1, HEAP32[$4 >> 2], HEAP32[$4 + 4 >> 2], $9)) { - return 0 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$4 + 8 | 0], $5)) { - return 0 - } - if (FLAC__bitwriter_write_zeroes($1, $7)) { - $2 = $2 + 1 | 0; - if ($2 >>> 0 >= HEAPU8[$8 | 0]) { - break label$16 - } - continue; - } - break; - }; - return 0; - } - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$0 + 164 >> 2]) { - continue - } - break; - }; - break label$3; - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 16 >> 2], HEAP32[1383])) { - break label$1 - } - $2 = strlen(HEAP32[$0 + 20 >> 2]); - if (!FLAC__bitwriter_write_raw_uint32($1, $2, HEAP32[1384])) { - break label$1 - } - if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 20 >> 2], $2)) { - break label$1 - } - $2 = strlen(HEAP32[$0 + 24 >> 2]); - if (!FLAC__bitwriter_write_raw_uint32($1, $2, HEAP32[1385])) { - break label$1 - } - if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 24 >> 2], $2)) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 28 >> 2], HEAP32[1386])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 32 >> 2], HEAP32[1387])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 36 >> 2], HEAP32[1388])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 40 >> 2], HEAP32[1389])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 44 >> 2], HEAP32[1390])) { - break label$1 - } - if (FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 48 >> 2], HEAP32[$0 + 44 >> 2])) { - break label$3 - } - break label$1; - } - if (!FLAC__bitwriter_write_byte_block($1, HEAP32[$0 + 16 >> 2], HEAP32[$0 + 8 >> 2])) { - break label$1 - } - } - $17 = 1; - } - return $17; - } - - function FLAC__frame_add_header($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $5 = global$0 - 16 | 0; - global$0 = $5; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[1394], HEAP32[1395])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, 0, HEAP32[1396])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 20 >> 2] != 0, HEAP32[1397])) { - break label$1 - } - $8 = 16; - $9 = 1; - $3 = $1; - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - label$8 : { - label$9 : { - label$10 : { - label$11 : { - $2 = HEAP32[$0 >> 2]; - if (($2 | 0) <= 2047) { - if (($2 | 0) <= 575) { - $4 = 1; - if (($2 | 0) == 192) { - break label$3 - } - if (($2 | 0) == 256) { - break label$8 - } - if (($2 | 0) != 512) { - break label$4 - } - $4 = 9; - break label$3; - } - if (($2 | 0) == 576) { - break label$11 - } - if (($2 | 0) == 1024) { - break label$7 - } - if (($2 | 0) != 1152) { - break label$4 - } - $4 = 3; - break label$3; - } - if (($2 | 0) <= 4607) { - if (($2 | 0) == 2048) { - break label$6 - } - if (($2 | 0) == 2304) { - break label$10 - } - if (($2 | 0) != 4096) { - break label$4 - } - $4 = 12; - break label$3; - } - if (($2 | 0) <= 16383) { - if (($2 | 0) == 4608) { - break label$9 - } - if (($2 | 0) != 8192) { - break label$4 - } - $4 = 13; - break label$3; - } - if (($2 | 0) == 16384) { - break label$5 - } - if (($2 | 0) != 32768) { - break label$4 - } - $4 = 15; - break label$3; - } - $4 = 2; - break label$3; - } - $4 = 4; - break label$3; - } - $4 = 5; - break label$3; - } - $4 = 8; - break label$3; - } - $4 = 10; - break label$3; - } - $4 = 11; - break label$3; - } - $4 = 14; - break label$3; - } - $2 = $2 >>> 0 < 257; - $8 = $2 ? 8 : 16; - $9 = 0; - $4 = $2 ? 6 : 7; - } - if (!FLAC__bitwriter_write_raw_uint32($3, $4, HEAP32[1398])) { - break label$1 - } - label$16 : { - label$17 : { - label$18 : { - label$19 : { - label$20 : { - label$21 : { - label$22 : { - label$23 : { - $2 = HEAP32[$0 + 4 >> 2]; - if (($2 | 0) <= 44099) { - if (($2 | 0) <= 22049) { - if (($2 | 0) == 8e3) { - break label$23 - } - if (($2 | 0) != 16e3) { - break label$17 - } - $3 = 5; - break label$16; - } - if (($2 | 0) == 22050) { - break label$22 - } - if (($2 | 0) == 24e3) { - break label$21 - } - if (($2 | 0) != 32e3) { - break label$17 - } - $3 = 8; - break label$16; - } - if (($2 | 0) <= 95999) { - if (($2 | 0) == 44100) { - break label$20 - } - if (($2 | 0) == 48e3) { - break label$19 - } - $3 = 1; - if (($2 | 0) == 88200) { - break label$16 - } - break label$17; - } - if (($2 | 0) == 96e3) { - break label$18 - } - if (($2 | 0) != 192e3) { - if (($2 | 0) != 176400) { - break label$17 - } - $3 = 2; - break label$16; - } - $3 = 3; - break label$16; - } - $3 = 4; - break label$16; - } - $3 = 6; - break label$16; - } - $3 = 7; - break label$16; - } - $3 = 9; - break label$16; - } - $3 = 10; - break label$16; - } - $3 = 11; - break label$16; - } - $6 = ($2 >>> 0) % 1e3 | 0; - if ($2 >>> 0 <= 255e3) { - $3 = 12; - $7 = 12; - if (!$6) { - break label$16 - } - } - if (!(($2 >>> 0) % 10)) { - $3 = 14; - $7 = 14; - break label$16; - } - $3 = $2 >>> 0 < 65536 ? 13 : 0; - $7 = $3; - } - $6 = 0; - if (!FLAC__bitwriter_write_raw_uint32($1, $3, HEAP32[1399])) { - break label$1 - } - label$30 : { - label$31 : { - switch (HEAP32[$0 + 12 >> 2]) { - case 0: - $3 = HEAP32[$0 + 8 >> 2] + -1 | 0; - break label$30; - case 1: - $3 = 8; - break label$30; - case 2: - $3 = 9; - break label$30; - case 3: - break label$31; - default: - break label$30; - }; - } - $3 = 10; - } - if (!FLAC__bitwriter_write_raw_uint32($1, $3, HEAP32[1400])) { - break label$1 - } - $3 = $1; - $2 = __wasm_rotl_i32(HEAP32[$0 + 16 >> 2] + -8 | 0, 30); - if ($2 >>> 0 <= 4) { - $2 = HEAP32[($2 << 2) + 10464 >> 2] - } else { - $2 = 0 - } - if (!FLAC__bitwriter_write_raw_uint32($3, $2, HEAP32[1401])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_uint32($1, 0, HEAP32[1402])) { - break label$1 - } - label$37 : { - if (!HEAP32[$0 + 20 >> 2]) { - if (FLAC__bitwriter_write_utf8_uint32($1, HEAP32[$0 + 24 >> 2])) { - break label$37 - } - break label$1; - } - if (!FLAC__bitwriter_write_utf8_uint64($1, HEAP32[$0 + 24 >> 2], HEAP32[$0 + 28 >> 2])) { - break label$1 - } - } - if (!$9) { - if (!FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 >> 2] + -1 | 0, $8)) { - break label$1 - } - } - label$40 : { - label$41 : { - switch ($7 + -12 | 0) { - case 0: - if (FLAC__bitwriter_write_raw_uint32($1, HEAPU32[$0 + 4 >> 2] / 1e3 | 0, 8)) { - break label$40 - } - break label$1; - case 1: - if (FLAC__bitwriter_write_raw_uint32($1, HEAP32[$0 + 4 >> 2], 16)) { - break label$40 - } - break label$1; - case 2: - break label$41; - default: - break label$40; - }; - } - if (!FLAC__bitwriter_write_raw_uint32($1, HEAPU32[$0 + 4 >> 2] / 10 | 0, 16)) { - break label$1 - } - } - if (!FLAC__bitwriter_get_write_crc8($1, $5 + 15 | 0)) { - break label$1 - } - $6 = (FLAC__bitwriter_write_raw_uint32($1, HEAPU8[$5 + 15 | 0], HEAP32[1403]) | 0) != 0; - } - global$0 = $5 + 16 | 0; - return $6; - } - - function FLAC__subframe_add_constant($0, $1, $2, $3) { - var $4 = 0; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($3, HEAP32[1417] | ($2 | 0) != 0, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { - break label$1 - } - if ($2) { - if (!FLAC__bitwriter_write_unary_unsigned($3, $2 + -1 | 0)) { - break label$1 - } - } - $4 = (FLAC__bitwriter_write_raw_int32($3, HEAP32[$0 >> 2], $1) | 0) != 0; - } - return $4; - } - - function FLAC__subframe_add_fixed($0, $1, $2, $3, $4) { - var $5 = 0; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[1419] | ($3 | 0) != 0 | HEAP32[$0 + 12 >> 2] << 1, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { - break label$1 - } - if ($3) { - if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { - break label$1 - } - } - label$3 : { - if (!HEAP32[$0 + 12 >> 2]) { - break label$3 - } - $3 = 0; - while (1) { - if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 16 >> 2], $2)) { - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { - continue - } - break label$3; - } - break; - }; - return 0; - } - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 >> 2], HEAP32[1405])) { - break label$1 - } - label$6 : { - if (HEAPU32[$0 >> 2] > 1) { - break label$6 - } - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 4 >> 2], HEAP32[1406])) { - break label$1 - } - $2 = HEAP32[$0 >> 2]; - if ($2 >>> 0 > 1) { - break label$6 - } - $3 = $1; - $1 = HEAP32[$0 + 8 >> 2]; - if (!add_residual_partitioned_rice_($4, HEAP32[$0 + 32 >> 2], $3, HEAP32[$0 + 12 >> 2], HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$0 + 4 >> 2], ($2 | 0) == 1)) { - break label$1 - } - } - $5 = 1; - } - return $5; - } - - function add_residual_partitioned_rice_($0, $1, $2, $3, $4, $5, $6, $7) { - var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; - $12 = HEAP32[($7 ? 5644 : 5640) >> 2]; - $9 = HEAP32[($7 ? 5632 : 5628) >> 2]; - label$1 : { - label$2 : { - if (!$6) { - if (!HEAP32[$5 >> 2]) { - if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$4 >> 2], $9)) { - break label$2 - } - if (!FLAC__bitwriter_write_rice_signed_block($0, $1, $2, HEAP32[$4 >> 2])) { - break label$2 - } - break label$1; - } - if (!FLAC__bitwriter_write_raw_uint32($0, $12, $9)) { - break label$2 - } - if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$5 >> 2], HEAP32[1409])) { - break label$2 - } - if (!$2) { - break label$1 - } - $7 = 0; - while (1) { - if (FLAC__bitwriter_write_raw_int32($0, HEAP32[($7 << 2) + $1 >> 2], HEAP32[$5 >> 2])) { - $7 = $7 + 1 | 0; - if (($7 | 0) != ($2 | 0)) { - continue - } - break label$1; - } - break; - }; - return 0; - } - $15 = $2 + $3 >>> $6 | 0; - $16 = HEAP32[1409]; - $2 = 0; - while (1) { - $7 = $2; - $13 = $15 - ($10 ? 0 : $3) | 0; - $2 = $7 + $13 | 0; - $14 = $10 << 2; - $8 = $14 + $5 | 0; - label$8 : { - if (!HEAP32[$8 >> 2]) { - $11 = 0; - $8 = $4 + $14 | 0; - if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$8 >> 2], $9)) { - break label$2 - } - if (FLAC__bitwriter_write_rice_signed_block($0, ($7 << 2) + $1 | 0, $13, HEAP32[$8 >> 2])) { - break label$8 - } - break label$2; - } - $11 = 0; - if (!FLAC__bitwriter_write_raw_uint32($0, $12, $9)) { - break label$2 - } - if (!FLAC__bitwriter_write_raw_uint32($0, HEAP32[$8 >> 2], $16)) { - break label$2 - } - if ($7 >>> 0 >= $2 >>> 0) { - break label$8 - } - while (1) { - if (!FLAC__bitwriter_write_raw_int32($0, HEAP32[($7 << 2) + $1 >> 2], HEAP32[$8 >> 2])) { - break label$2 - } - $7 = $7 + 1 | 0; - if (($7 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - $11 = 1; - $10 = $10 + 1 | 0; - if (!($10 >>> $6)) { - continue - } - break; - }; - } - return $11; - } - return 1; - } - - function FLAC__subframe_add_lpc($0, $1, $2, $3, $4) { - var $5 = 0; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($4, (HEAP32[$0 + 12 >> 2] << 1) + -2 | (HEAP32[1420] | ($3 | 0) != 0), HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { - break label$1 - } - if ($3) { - if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { - break label$1 - } - } - label$3 : { - if (!HEAP32[$0 + 12 >> 2]) { - break label$3 - } - $3 = 0; - while (1) { - if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 152 >> 2], $2)) { - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { - continue - } - break label$3; - } - break; - }; - return 0; - } - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 16 >> 2] + -1 | 0, HEAP32[1412])) { - break label$1 - } - if (!FLAC__bitwriter_write_raw_int32($4, HEAP32[$0 + 20 >> 2], HEAP32[1413])) { - break label$1 - } - label$6 : { - if (!HEAP32[$0 + 12 >> 2]) { - break label$6 - } - $3 = 0; - while (1) { - if (FLAC__bitwriter_write_raw_int32($4, HEAP32[(($3 << 2) + $0 | 0) + 24 >> 2], HEAP32[$0 + 16 >> 2])) { - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$0 + 12 >> 2]) { - continue - } - break label$6; - } - break; - }; - return 0; - } - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 >> 2], HEAP32[1405])) { - break label$1 - } - label$9 : { - if (HEAPU32[$0 >> 2] > 1) { - break label$9 - } - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[$0 + 4 >> 2], HEAP32[1406])) { - break label$1 - } - $2 = HEAP32[$0 >> 2]; - if ($2 >>> 0 > 1) { - break label$9 - } - $3 = $1; - $1 = HEAP32[$0 + 8 >> 2]; - if (!add_residual_partitioned_rice_($4, HEAP32[$0 + 280 >> 2], $3, HEAP32[$0 + 12 >> 2], HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$0 + 4 >> 2], ($2 | 0) == 1)) { - break label$1 - } - } - $5 = 1; - } - return $5; - } - - function FLAC__subframe_add_verbatim($0, $1, $2, $3, $4) { - $0 = HEAP32[$0 >> 2]; - label$1 : { - if (!FLAC__bitwriter_write_raw_uint32($4, HEAP32[1418] | ($3 | 0) != 0, HEAP32[1416] + (HEAP32[1415] + HEAP32[1414] | 0) | 0)) { - break label$1 - } - if ($3) { - if (!FLAC__bitwriter_write_unary_unsigned($4, $3 + -1 | 0)) { - break label$1 - } - } - if (!$1) { - return 1 - } - $3 = 0; - label$4 : { - while (1) { - if (!FLAC__bitwriter_write_raw_int32($4, HEAP32[$0 + ($3 << 2) >> 2], $2)) { - break label$4 - } - $3 = $3 + 1 | 0; - if (($3 | 0) != ($1 | 0)) { - continue - } - break; - }; - return 1; - } - } - return 0; - } - - function strncmp($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0; - if (!$2) { - return 0 - } - $3 = HEAPU8[$0 | 0]; - label$2 : { - if (!$3) { - break label$2 - } - while (1) { - label$4 : { - $4 = HEAPU8[$1 | 0]; - if (($4 | 0) != ($3 | 0)) { - break label$4 - } - $2 = $2 + -1 | 0; - if (!$2 | !$4) { - break label$4 - } - $1 = $1 + 1 | 0; - $3 = HEAPU8[$0 + 1 | 0]; - $0 = $0 + 1 | 0; - if ($3) { - continue - } - break label$2; - } - break; - }; - $5 = $3; - } - return ($5 & 255) - HEAPU8[$1 | 0] | 0; - } - - function __uflow($0) { - var $1 = 0, $2 = 0; - $1 = global$0 - 16 | 0; - global$0 = $1; - $2 = -1; - label$1 : { - if (__toread($0)) { - break label$1 - } - if ((FUNCTION_TABLE[HEAP32[$0 + 32 >> 2]]($0, $1 + 15 | 0, 1) | 0) != 1) { - break label$1 - } - $2 = HEAPU8[$1 + 15 | 0]; - } - global$0 = $1 + 16 | 0; - return $2; - } - - function __shlim($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0; - HEAP32[$0 + 112 >> 2] = 0; - HEAP32[$0 + 116 >> 2] = 0; - $3 = HEAP32[$0 + 8 >> 2]; - $4 = HEAP32[$0 + 4 >> 2]; - $1 = $3 - $4 | 0; - $2 = $1 >> 31; - HEAP32[$0 + 120 >> 2] = $1; - HEAP32[$0 + 124 >> 2] = $2; - if (!((($2 | 0) < 0 ? 1 : ($2 | 0) <= 0 ? ($1 >>> 0 > 0 ? 0 : 1) : 0) | 1)) { - HEAP32[$0 + 104 >> 2] = $4; - return; - } - HEAP32[$0 + 104 >> 2] = $3; - } - - function __shgetc($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $2 = HEAP32[$0 + 116 >> 2]; - $3 = $2; - label$1 : { - $5 = HEAP32[$0 + 112 >> 2]; - label$2 : { - if ($2 | $5) { - $2 = HEAP32[$0 + 124 >> 2]; - if (($2 | 0) > ($3 | 0) ? 1 : ($2 | 0) >= ($3 | 0) ? (HEAPU32[$0 + 120 >> 2] < $5 >>> 0 ? 0 : 1) : 0) { - break label$2 - } - } - $5 = __uflow($0); - if (($5 | 0) > -1) { - break label$1 - } - } - HEAP32[$0 + 104 >> 2] = 0; - return -1; - } - $2 = HEAP32[$0 + 8 >> 2]; - $3 = HEAP32[$0 + 116 >> 2]; - $4 = $3; - label$4 : { - label$5 : { - $1 = HEAP32[$0 + 112 >> 2]; - if (!($3 | $1)) { - break label$5 - } - $3 = (HEAP32[$0 + 124 >> 2] ^ -1) + $4 | 0; - $4 = HEAP32[$0 + 120 >> 2] ^ -1; - $1 = $4 + $1 | 0; - if ($1 >>> 0 < $4 >>> 0) { - $3 = $3 + 1 | 0 - } - $4 = $1; - $1 = HEAP32[$0 + 4 >> 2]; - $6 = $2 - $1 | 0; - $7 = $4 >>> 0 < $6 >>> 0 ? 0 : 1; - $6 = $6 >> 31; - if (($3 | 0) > ($6 | 0) ? 1 : ($3 | 0) >= ($6 | 0) ? $7 : 0) { - break label$5 - } - HEAP32[$0 + 104 >> 2] = $4 + $1; - break label$4; - } - HEAP32[$0 + 104 >> 2] = $2; - } - label$6 : { - if (!$2) { - $2 = HEAP32[$0 + 4 >> 2]; - break label$6; - } - $3 = $0; - $1 = $2; - $2 = HEAP32[$0 + 4 >> 2]; - $1 = ($1 - $2 | 0) + 1 | 0; - $4 = $1 + HEAP32[$0 + 120 >> 2] | 0; - $0 = HEAP32[$0 + 124 >> 2] + ($1 >> 31) | 0; - HEAP32[$3 + 120 >> 2] = $4; - HEAP32[$3 + 124 >> 2] = $4 >>> 0 < $1 >>> 0 ? $0 + 1 | 0 : $0; - } - $0 = $2 + -1 | 0; - if (HEAPU8[$0 | 0] != ($5 | 0)) { - HEAP8[$0 | 0] = $5 - } - return $5; - } - - function __extendsftf2($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - $4 = global$0 - 16 | 0; - global$0 = $4; - $5 = (wasm2js_scratch_store_f32($1), wasm2js_scratch_load_i32(0)); - $2 = $5 & 2147483647; - label$1 : { - if ($2 + -8388608 >>> 0 <= 2130706431) { - $3 = $2; - $2 = $2 >>> 7 | 0; - $3 = $3 << 25; - $2 = $2 + 1065353216 | 0; - $6 = $3; - $2 = $3 >>> 0 < 0 ? $2 + 1 | 0 : $2; - break label$1; - } - if ($2 >>> 0 >= 2139095040) { - $2 = $5; - $3 = $2 >>> 7 | 0; - $6 = $2 << 25; - $2 = $3 | 2147418112; - break label$1; - } - if (!$2) { - $2 = 0; - break label$1; - } - $3 = $2; - $2 = Math_clz32($2); - __ashlti3($4, $3, 0, 0, 0, $2 + 81 | 0); - $7 = HEAP32[$4 >> 2]; - $8 = HEAP32[$4 + 4 >> 2]; - $6 = HEAP32[$4 + 8 >> 2]; - $2 = HEAP32[$4 + 12 >> 2] ^ 65536 | 16265 - $2 << 16; - } - HEAP32[$0 >> 2] = $7; - HEAP32[$0 + 4 >> 2] = $8; - HEAP32[$0 + 8 >> 2] = $6; - HEAP32[$0 + 12 >> 2] = $5 & -2147483648 | $2; - global$0 = $4 + 16 | 0; - } - - function __floatsitf($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $3 = global$0 - 16 | 0; - global$0 = $3; - $6 = $0; - $7 = $0; - label$1 : { - if (!$1) { - $1 = 0; - $5 = 0; - break label$1; - } - $2 = $1 >> 31; - $4 = $2 + $1 ^ $2; - $2 = Math_clz32($4); - __ashlti3($3, $4, 0, 0, 0, $2 + 81 | 0); - $2 = (HEAP32[$3 + 12 >> 2] ^ 65536) + (16414 - $2 << 16) | 0; - $4 = 0 + HEAP32[$3 + 8 >> 2] | 0; - if ($4 >>> 0 < $5 >>> 0) { - $2 = $2 + 1 | 0 - } - $1 = $1 & -2147483648 | $2; - $2 = HEAP32[$3 + 4 >> 2]; - $5 = HEAP32[$3 >> 2]; - } - HEAP32[$7 >> 2] = $5; - HEAP32[$6 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $4; - HEAP32[$0 + 12 >> 2] = $1; - global$0 = $3 + 16 | 0; - } - - function __multf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0, $46 = 0, $47 = 0; - $13 = global$0 - 96 | 0; - global$0 = $13; - $15 = $2; - $10 = $6; - $19 = ($10 & 131071) << 15 | $5 >>> 17; - $9 = $8 & 65535; - $21 = $9; - $17 = $7; - $10 = $7; - $24 = $10 << 15 | $6 >>> 17; - $14 = ($4 ^ $8) & -2147483648; - $10 = $4 & 65535; - $12 = $10; - $16 = $3; - $27 = $10; - $10 = $9; - $25 = ($10 & 131071) << 15 | $7 >>> 17; - $37 = $8 >>> 16 & 32767; - $38 = $4 >>> 16 & 32767; - label$1 : { - label$2 : { - if ($38 + -1 >>> 0 <= 32765) { - $20 = 0; - if ($37 + -1 >>> 0 < 32766) { - break label$2 - } - } - $11 = $4 & 2147483647; - $9 = $11; - $10 = $3; - if (!(!$3 & ($9 | 0) == 2147418112 ? !($1 | $2) : ($9 | 0) == 2147418112 & $3 >>> 0 < 0 | $9 >>> 0 < 2147418112)) { - $22 = $3; - $14 = $4 | 32768; - break label$1; - } - $11 = $8 & 2147483647; - $4 = $11; - $3 = $7; - if (!(!$3 & ($4 | 0) == 2147418112 ? !($5 | $6) : ($4 | 0) == 2147418112 & $3 >>> 0 < 0 | $4 >>> 0 < 2147418112)) { - $22 = $7; - $14 = $8 | 32768; - $1 = $5; - $2 = $6; - break label$1; - } - if (!($1 | $10 | ($9 ^ 2147418112 | $2))) { - if (!($3 | $5 | ($4 | $6))) { - $14 = 2147450880; - $1 = 0; - $2 = 0; - break label$1; - } - $14 = $14 | 2147418112; - $1 = 0; - $2 = 0; - break label$1; - } - if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { - $3 = $1 | $10; - $4 = $2 | $9; - $1 = 0; - $2 = 0; - if (!($3 | $4)) { - $14 = 2147450880; - break label$1; - } - $14 = $14 | 2147418112; - break label$1; - } - if (!($1 | $10 | ($2 | $9))) { - $1 = 0; - $2 = 0; - break label$1; - } - if (!($3 | $5 | ($4 | $6))) { - $1 = 0; - $2 = 0; - break label$1; - } - $3 = 0; - if (($9 | 0) == 65535 | $9 >>> 0 < 65535) { - $9 = $1; - $8 = $2; - $3 = !($12 | $16); - $7 = $3 << 6; - $10 = Math_clz32($3 ? $1 : $16) + 32 | 0; - $1 = Math_clz32($3 ? $2 : $12); - $1 = $7 + (($1 | 0) == 32 ? $10 : $1) | 0; - __ashlti3($13 + 80 | 0, $9, $8, $16, $12, $1 + -15 | 0); - $16 = HEAP32[$13 + 88 >> 2]; - $15 = HEAP32[$13 + 84 >> 2]; - $27 = HEAP32[$13 + 92 >> 2]; - $3 = 16 - $1 | 0; - $1 = HEAP32[$13 + 80 >> 2]; - } - $20 = $3; - if ($4 >>> 0 > 65535) { - break label$2 - } - $2 = !($17 | $21); - $4 = $2 << 6; - $7 = Math_clz32($2 ? $5 : $17) + 32 | 0; - $2 = Math_clz32($2 ? $6 : $21); - $2 = $4 + (($2 | 0) == 32 ? $7 : $2) | 0; - $8 = $2; - __ashlti3($13 - -64 | 0, $5, $6, $17, $21, $2 + -15 | 0); - $5 = HEAP32[$13 + 76 >> 2]; - $2 = $5; - $7 = HEAP32[$13 + 72 >> 2]; - $4 = $7; - $4 = $4 << 15; - $10 = HEAP32[$13 + 68 >> 2]; - $24 = $10 >>> 17 | $4; - $4 = $10; - $5 = HEAP32[$13 + 64 >> 2]; - $19 = ($4 & 131071) << 15 | $5 >>> 17; - $25 = ($2 & 131071) << 15 | $7 >>> 17; - $20 = ($3 - $8 | 0) + 16 | 0; - } - $3 = $19; - $17 = 0; - $8 = __wasm_i64_mul($3, 0, $1, $17); - $2 = i64toi32_i32$HIGH_BITS; - $26 = $2; - $23 = $5 << 15 & -32768; - $5 = __wasm_i64_mul($23, 0, $15, 0); - $4 = $5 + $8 | 0; - $11 = i64toi32_i32$HIGH_BITS + $2 | 0; - $11 = $4 >>> 0 < $5 >>> 0 ? $11 + 1 | 0 : $11; - $2 = $4; - $5 = 0; - $6 = __wasm_i64_mul($23, $28, $1, $17); - $4 = $5 + $6 | 0; - $9 = i64toi32_i32$HIGH_BITS + $2 | 0; - $9 = $4 >>> 0 < $6 >>> 0 ? $9 + 1 | 0 : $9; - $19 = $4; - $6 = $9; - $32 = ($2 | 0) == ($9 | 0) & $4 >>> 0 < $5 >>> 0 | $9 >>> 0 < $2 >>> 0; - $41 = __wasm_i64_mul($3, $39, $15, $40); - $33 = i64toi32_i32$HIGH_BITS; - $29 = $16; - $5 = __wasm_i64_mul($23, $28, $16, 0); - $4 = $5 + $41 | 0; - $12 = i64toi32_i32$HIGH_BITS + $33 | 0; - $12 = $4 >>> 0 < $5 >>> 0 ? $12 + 1 | 0 : $12; - $42 = $4; - $7 = __wasm_i64_mul($24, 0, $1, $17); - $4 = $4 + $7 | 0; - $5 = i64toi32_i32$HIGH_BITS + $12 | 0; - $34 = $4; - $5 = $4 >>> 0 < $7 >>> 0 ? $5 + 1 | 0 : $5; - $21 = $5; - $7 = $5; - $5 = ($11 | 0) == ($26 | 0) & $2 >>> 0 < $8 >>> 0 | $11 >>> 0 < $26 >>> 0; - $4 = $11; - $2 = $4 + $34 | 0; - $9 = $5 + $7 | 0; - $26 = $2; - $9 = $2 >>> 0 < $4 >>> 0 ? $9 + 1 | 0 : $9; - $4 = $9; - $7 = $2; - $44 = __wasm_i64_mul($3, $39, $16, $43); - $35 = i64toi32_i32$HIGH_BITS; - $2 = $23; - $30 = $27 | 65536; - $23 = $18; - $5 = __wasm_i64_mul($2, $28, $30, $18); - $2 = $5 + $44 | 0; - $9 = i64toi32_i32$HIGH_BITS + $35 | 0; - $9 = $2 >>> 0 < $5 >>> 0 ? $9 + 1 | 0 : $9; - $45 = $2; - $10 = __wasm_i64_mul($15, $40, $24, $46); - $2 = $2 + $10 | 0; - $18 = $9; - $5 = $9 + i64toi32_i32$HIGH_BITS | 0; - $5 = $2 >>> 0 < $10 >>> 0 ? $5 + 1 | 0 : $5; - $36 = $2; - $31 = $25 & 2147483647 | -2147483648; - $2 = __wasm_i64_mul($31, 0, $1, $17); - $1 = $36 + $2 | 0; - $17 = $5; - $10 = $5 + i64toi32_i32$HIGH_BITS | 0; - $28 = $1; - $2 = $1 >>> 0 < $2 >>> 0 ? $10 + 1 | 0 : $10; - $9 = $4 + $1 | 0; - $5 = 0; - $1 = $5 + $7 | 0; - if ($1 >>> 0 < $5 >>> 0) { - $9 = $9 + 1 | 0 - } - $27 = $1; - $25 = $9; - $5 = $9; - $7 = $1 + $32 | 0; - if ($7 >>> 0 < $1 >>> 0) { - $5 = $5 + 1 | 0 - } - $8 = $5; - $16 = ($20 + ($37 + $38 | 0) | 0) + -16383 | 0; - $5 = __wasm_i64_mul($29, $43, $24, $46); - $1 = i64toi32_i32$HIGH_BITS; - $11 = 0; - $10 = __wasm_i64_mul($3, $39, $30, $23); - $3 = $10 + $5 | 0; - $9 = i64toi32_i32$HIGH_BITS + $1 | 0; - $9 = $3 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; - $20 = $3; - $10 = $3; - $3 = $9; - $9 = ($1 | 0) == ($3 | 0) & $10 >>> 0 < $5 >>> 0 | $3 >>> 0 < $1 >>> 0; - $5 = __wasm_i64_mul($31, $47, $15, $40); - $1 = $5 + $10 | 0; - $10 = i64toi32_i32$HIGH_BITS + $3 | 0; - $10 = $1 >>> 0 < $5 >>> 0 ? $10 + 1 | 0 : $10; - $15 = $1; - $5 = $1; - $1 = $10; - $3 = ($3 | 0) == ($1 | 0) & $5 >>> 0 < $20 >>> 0 | $1 >>> 0 < $3 >>> 0; - $5 = $9 + $3 | 0; - if ($5 >>> 0 < $3 >>> 0) { - $11 = 1 - } - $10 = $5; - $3 = $1; - $5 = $11; - $32 = $10; - $9 = 0; - $10 = ($12 | 0) == ($21 | 0) & $34 >>> 0 < $42 >>> 0 | $21 >>> 0 < $12 >>> 0; - $12 = $10 + (($12 | 0) == ($33 | 0) & $42 >>> 0 < $41 >>> 0 | $12 >>> 0 < $33 >>> 0) | 0; - if ($12 >>> 0 < $10 >>> 0) { - $9 = 1 - } - $11 = $12; - $12 = $12 + $15 | 0; - $10 = $3 + $9 | 0; - $20 = $12; - $9 = $12; - $10 = $9 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; - $3 = $10; - $1 = ($1 | 0) == ($3 | 0) & $9 >>> 0 < $15 >>> 0 | $3 >>> 0 < $1 >>> 0; - $10 = $32 + $1 | 0; - if ($10 >>> 0 < $1 >>> 0) { - $5 = $5 + 1 | 0 - } - $1 = $10; - $10 = __wasm_i64_mul($31, $47, $30, $23); - $1 = $1 + $10 | 0; - $9 = i64toi32_i32$HIGH_BITS + $5 | 0; - $9 = $1 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; - $11 = $1; - $12 = __wasm_i64_mul($31, $47, $29, $43); - $5 = i64toi32_i32$HIGH_BITS; - $15 = __wasm_i64_mul($24, $46, $30, $23); - $1 = $15 + $12 | 0; - $10 = i64toi32_i32$HIGH_BITS + $5 | 0; - $10 = $1 >>> 0 < $15 >>> 0 ? $10 + 1 | 0 : $10; - $15 = $1; - $1 = $10; - $10 = ($5 | 0) == ($1 | 0) & $15 >>> 0 < $12 >>> 0 | $1 >>> 0 < $5 >>> 0; - $5 = $1 + $11 | 0; - $11 = $9 + $10 | 0; - $10 = $5 >>> 0 < $1 >>> 0 ? $11 + 1 | 0 : $11; - $29 = $5; - $9 = $3 + $15 | 0; - $11 = 0; - $1 = $11 + $20 | 0; - if ($1 >>> 0 < $11 >>> 0) { - $9 = $9 + 1 | 0 - } - $12 = $1; - $5 = $1; - $1 = $9; - $3 = ($3 | 0) == ($1 | 0) & $5 >>> 0 < $20 >>> 0 | $1 >>> 0 < $3 >>> 0; - $5 = $29 + $3 | 0; - if ($5 >>> 0 < $3 >>> 0) { - $10 = $10 + 1 | 0 - } - $15 = $5; - $11 = $1; - $9 = 0; - $5 = ($18 | 0) == ($17 | 0) & $36 >>> 0 < $45 >>> 0 | $17 >>> 0 < $18 >>> 0; - $18 = $5 + (($18 | 0) == ($35 | 0) & $45 >>> 0 < $44 >>> 0 | $18 >>> 0 < $35 >>> 0) | 0; - if ($18 >>> 0 < $5 >>> 0) { - $9 = 1 - } - $5 = $18 + (($2 | 0) == ($17 | 0) & $28 >>> 0 < $36 >>> 0 | $2 >>> 0 < $17 >>> 0) | 0; - $3 = $2; - $2 = $3 + $12 | 0; - $11 = $5 + $11 | 0; - $11 = $2 >>> 0 < $3 >>> 0 ? $11 + 1 | 0 : $11; - $18 = $2; - $3 = $2; - $2 = $11; - $1 = ($1 | 0) == ($2 | 0) & $3 >>> 0 < $12 >>> 0 | $2 >>> 0 < $1 >>> 0; - $3 = $1 + $15 | 0; - if ($3 >>> 0 < $1 >>> 0) { - $10 = $10 + 1 | 0 - } - $1 = $2; - $9 = $10; - $10 = $3; - $5 = 0; - $3 = ($4 | 0) == ($25 | 0) & $27 >>> 0 < $26 >>> 0 | $25 >>> 0 < $4 >>> 0; - $4 = $3 + (($4 | 0) == ($21 | 0) & $26 >>> 0 < $34 >>> 0 | $4 >>> 0 < $21 >>> 0) | 0; - if ($4 >>> 0 < $3 >>> 0) { - $5 = 1 - } - $3 = $4 + $18 | 0; - $11 = $1 + $5 | 0; - $11 = $3 >>> 0 < $4 >>> 0 ? $11 + 1 | 0 : $11; - $1 = $3; - $4 = $11; - $1 = ($2 | 0) == ($4 | 0) & $1 >>> 0 < $18 >>> 0 | $4 >>> 0 < $2 >>> 0; - $2 = $10 + $1 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $9 = $9 + 1 | 0 - } - $1 = $2; - $2 = $9; - label$13 : { - if ($2 & 65536) { - $16 = $16 + 1 | 0; - break label$13; - } - $12 = $6 >>> 31 | 0; - $9 = $2 << 1 | $1 >>> 31; - $1 = $1 << 1 | $4 >>> 31; - $2 = $9; - $9 = $4 << 1 | $3 >>> 31; - $3 = $3 << 1 | $8 >>> 31; - $4 = $9; - $10 = $19; - $9 = $6 << 1 | $10 >>> 31; - $19 = $10 << 1; - $6 = $9; - $10 = $8 << 1 | $7 >>> 31; - $7 = $7 << 1 | $12; - $8 = $10; - } - if (($16 | 0) >= 32767) { - $14 = $14 | 2147418112; - $1 = 0; - $2 = 0; - break label$1; - } - label$16 : { - if (($16 | 0) <= 0) { - $5 = 1 - $16 | 0; - if ($5 >>> 0 <= 127) { - $10 = $16 + 127 | 0; - __ashlti3($13 + 48 | 0, $19, $6, $7, $8, $10); - __ashlti3($13 + 32 | 0, $3, $4, $1, $2, $10); - __lshrti3($13 + 16 | 0, $19, $6, $7, $8, $5); - __lshrti3($13, $3, $4, $1, $2, $5); - $19 = (HEAP32[$13 + 48 >> 2] | HEAP32[$13 + 56 >> 2]) != 0 | (HEAP32[$13 + 52 >> 2] | HEAP32[$13 + 60 >> 2]) != 0 | (HEAP32[$13 + 32 >> 2] | HEAP32[$13 + 16 >> 2]); - $6 = HEAP32[$13 + 36 >> 2] | HEAP32[$13 + 20 >> 2]; - $7 = HEAP32[$13 + 40 >> 2] | HEAP32[$13 + 24 >> 2]; - $8 = HEAP32[$13 + 44 >> 2] | HEAP32[$13 + 28 >> 2]; - $3 = HEAP32[$13 >> 2]; - $4 = HEAP32[$13 + 4 >> 2]; - $2 = HEAP32[$13 + 12 >> 2]; - $1 = HEAP32[$13 + 8 >> 2]; - break label$16; - } - $1 = 0; - $2 = 0; - break label$1; - } - $2 = $2 & 65535 | $16 << 16; - } - $22 = $1 | $22; - $14 = $2 | $14; - if (!(!$7 & ($8 | 0) == -2147483648 ? !($6 | $19) : ($8 | 0) > -1 ? 1 : 0)) { - $11 = $14; - $12 = $4; - $1 = $3 + 1 | 0; - if ($1 >>> 0 < 1) { - $12 = $12 + 1 | 0 - } - $2 = $12; - $3 = ($4 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $4 >>> 0; - $4 = $3 + $22 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $11 = $11 + 1 | 0 - } - $22 = $4; - $14 = $11; - break label$1; - } - if ($7 | $19 | ($8 ^ -2147483648 | $6)) { - $1 = $3; - $2 = $4; - break label$1; - } - $12 = $14; - $9 = $4; - $1 = $3 & 1; - $2 = $1 + $3 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $9 = $9 + 1 | 0 - } - $1 = $2; - $2 = $9; - $3 = ($4 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $4 >>> 0; - $4 = $3 + $22 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $12 = $12 + 1 | 0 - } - $22 = $4; - $14 = $12; - } - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $22; - HEAP32[$0 + 12 >> 2] = $14; - global$0 = $13 + 96 | 0; - } - - function __addtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0; - $11 = global$0 - 112 | 0; - global$0 = $11; - $12 = $7; - $14 = $8 & 2147483647; - $10 = $2 + -1 | 0; - $9 = $1 + -1 | 0; - if (($9 | 0) != -1) { - $10 = $10 + 1 | 0 - } - $13 = $9; - $17 = ($9 | 0) == -1 & ($10 | 0) == -1; - $15 = $4 & 2147483647; - $9 = $15; - $16 = $3; - $10 = ($2 | 0) == ($10 | 0) & $13 >>> 0 < $1 >>> 0 | $10 >>> 0 < $2 >>> 0; - $13 = $3 + $10 | 0; - if ($13 >>> 0 < $10 >>> 0) { - $9 = $9 + 1 | 0 - } - $13 = $13 + -1 | 0; - $10 = $9 + -1 | 0; - $9 = $13; - label$1 : { - label$2 : { - $10 = ($9 | 0) != -1 ? $10 + 1 | 0 : $10; - if (!(($9 | 0) == -1 & ($10 | 0) == 2147418111 ? $17 : $10 >>> 0 > 2147418111)) { - $10 = $6 + -1 | 0; - $9 = $5 + -1 | 0; - if (($9 | 0) != -1) { - $10 = $10 + 1 | 0 - } - $13 = $9; - $17 = ($9 | 0) != -1 | ($10 | 0) != -1; - $9 = $14; - $10 = ($6 | 0) == ($10 | 0) & $13 >>> 0 < $5 >>> 0 | $10 >>> 0 < $6 >>> 0; - $13 = $10 + $12 | 0; - if ($13 >>> 0 < $10 >>> 0) { - $9 = $9 + 1 | 0 - } - $10 = $13 + -1 | 0; - $9 = $9 + -1 | 0; - $9 = ($10 | 0) != -1 ? $9 + 1 | 0 : $9; - if (($10 | 0) == -1 & ($9 | 0) == 2147418111 ? $17 : ($9 | 0) == 2147418111 & ($10 | 0) != -1 | $9 >>> 0 < 2147418111) { - break label$2 - } - } - if (!(!$16 & ($15 | 0) == 2147418112 ? !($1 | $2) : ($15 | 0) == 2147418112 & $16 >>> 0 < 0 | $15 >>> 0 < 2147418112)) { - $7 = $3; - $8 = $4 | 32768; - $5 = $1; - $6 = $2; - break label$1; - } - if (!(!$12 & ($14 | 0) == 2147418112 ? !($5 | $6) : ($14 | 0) == 2147418112 & $12 >>> 0 < 0 | $14 >>> 0 < 2147418112)) { - $8 = $8 | 32768; - break label$1; - } - if (!($1 | $16 | ($15 ^ 2147418112 | $2))) { - $9 = $3; - $3 = !($1 ^ $5 | $3 ^ $7 | ($2 ^ $6 | $4 ^ $8 ^ -2147483648)); - $7 = $3 ? 0 : $9; - $8 = $3 ? 2147450880 : $4; - $5 = $3 ? 0 : $1; - $6 = $3 ? 0 : $2; - break label$1; - } - if (!($5 | $12 | ($14 ^ 2147418112 | $6))) { - break label$1 - } - if (!($1 | $16 | ($2 | $15))) { - if ($5 | $12 | ($6 | $14)) { - break label$1 - } - $5 = $1 & $5; - $6 = $2 & $6; - $7 = $3 & $7; - $8 = $4 & $8; - break label$1; - } - if ($5 | $12 | ($6 | $14)) { - break label$2 - } - $5 = $1; - $6 = $2; - $7 = $3; - $8 = $4; - break label$1; - } - $10 = ($12 | 0) == ($16 | 0) & ($14 | 0) == ($15 | 0) ? ($2 | 0) == ($6 | 0) & $5 >>> 0 > $1 >>> 0 | $6 >>> 0 > $2 >>> 0 : ($14 | 0) == ($15 | 0) & $12 >>> 0 > $16 >>> 0 | $14 >>> 0 > $15 >>> 0; - $9 = $10; - $15 = $9 ? $5 : $1; - $14 = $9 ? $6 : $2; - $12 = $9 ? $8 : $4; - $16 = $12; - $13 = $9 ? $7 : $3; - $9 = $12 & 65535; - $4 = $10 ? $4 : $8; - $18 = $4; - $3 = $10 ? $3 : $7; - $17 = $4 >>> 16 & 32767; - $12 = $12 >>> 16 & 32767; - if (!$12) { - $4 = !($9 | $13); - $7 = $4 << 6; - $8 = Math_clz32($4 ? $15 : $13) + 32 | 0; - $4 = Math_clz32($4 ? $14 : $9); - $4 = $7 + (($4 | 0) == 32 ? $8 : $4) | 0; - __ashlti3($11 + 96 | 0, $15, $14, $13, $9, $4 + -15 | 0); - $13 = HEAP32[$11 + 104 >> 2]; - $15 = HEAP32[$11 + 96 >> 2]; - $14 = HEAP32[$11 + 100 >> 2]; - $12 = 16 - $4 | 0; - $9 = HEAP32[$11 + 108 >> 2]; - } - $5 = $10 ? $1 : $5; - $6 = $10 ? $2 : $6; - $1 = $3; - $2 = $18 & 65535; - if ($17) { - $1 = $2 - } else { - $7 = $1; - $3 = !($1 | $2); - $4 = $3 << 6; - $8 = Math_clz32($3 ? $5 : $1) + 32 | 0; - $1 = Math_clz32($3 ? $6 : $2); - $1 = $4 + (($1 | 0) == 32 ? $8 : $1) | 0; - __ashlti3($11 + 80 | 0, $5, $6, $7, $2, $1 + -15 | 0); - $17 = 16 - $1 | 0; - $5 = HEAP32[$11 + 80 >> 2]; - $6 = HEAP32[$11 + 84 >> 2]; - $3 = HEAP32[$11 + 88 >> 2]; - $1 = HEAP32[$11 + 92 >> 2]; - } - $2 = $3; - $10 = $1 << 3 | $2 >>> 29; - $7 = $2 << 3 | $6 >>> 29; - $8 = $10 | 524288; - $1 = $13; - $3 = $9 << 3 | $1 >>> 29; - $4 = $1 << 3 | $14 >>> 29; - $13 = $3; - $10 = $16 ^ $18; - $1 = $5; - $9 = $6 << 3 | $1 >>> 29; - $1 = $1 << 3; - $2 = $9; - $5 = $12 - $17 | 0; - $3 = $1; - label$11 : { - if (!$5) { - break label$11 - } - if ($5 >>> 0 > 127) { - $7 = 0; - $8 = 0; - $9 = 0; - $3 = 1; - break label$11; - } - __ashlti3($11 - -64 | 0, $1, $2, $7, $8, 128 - $5 | 0); - __lshrti3($11 + 48 | 0, $1, $2, $7, $8, $5); - $7 = HEAP32[$11 + 56 >> 2]; - $8 = HEAP32[$11 + 60 >> 2]; - $9 = HEAP32[$11 + 52 >> 2]; - $3 = HEAP32[$11 + 48 >> 2] | ((HEAP32[$11 + 64 >> 2] | HEAP32[$11 + 72 >> 2]) != 0 | (HEAP32[$11 + 68 >> 2] | HEAP32[$11 + 76 >> 2]) != 0); - } - $6 = $9; - $13 = $13 | 524288; - $1 = $15; - $9 = $14 << 3 | $1 >>> 29; - $2 = $1 << 3; - label$13 : { - if (($10 | 0) < -1 ? 1 : ($10 | 0) <= -1 ? 1 : 0) { - $14 = $3; - $1 = $2 - $3 | 0; - $15 = $4 - $7 | 0; - $3 = ($6 | 0) == ($9 | 0) & $2 >>> 0 < $3 >>> 0 | $9 >>> 0 < $6 >>> 0; - $5 = $15 - $3 | 0; - $2 = $9 - (($2 >>> 0 < $14 >>> 0) + $6 | 0) | 0; - $6 = ($13 - (($4 >>> 0 < $7 >>> 0) + $8 | 0) | 0) - ($15 >>> 0 < $3 >>> 0) | 0; - if (!($1 | $5 | ($2 | $6))) { - $5 = 0; - $6 = 0; - $7 = 0; - $8 = 0; - break label$1; - } - if ($6 >>> 0 > 524287) { - break label$13 - } - $7 = $1; - $3 = !($5 | $6); - $4 = $3 << 6; - $8 = Math_clz32($3 ? $1 : $5) + 32 | 0; - $1 = Math_clz32($3 ? $2 : $6); - $1 = $4 + (($1 | 0) == 32 ? $8 : $1) | 0; - $1 = $1 + -12 | 0; - __ashlti3($11 + 32 | 0, $7, $2, $5, $6, $1); - $12 = $12 - $1 | 0; - $5 = HEAP32[$11 + 40 >> 2]; - $6 = HEAP32[$11 + 44 >> 2]; - $1 = HEAP32[$11 + 32 >> 2]; - $2 = HEAP32[$11 + 36 >> 2]; - break label$13; - } - $10 = $6 + $9 | 0; - $1 = $3; - $2 = $1 + $2 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $10 = $10 + 1 | 0 - } - $1 = $2; - $2 = $10; - $6 = ($6 | 0) == ($2 | 0) & $1 >>> 0 < $3 >>> 0 | $2 >>> 0 < $6 >>> 0; - $10 = $8 + $13 | 0; - $3 = $4 + $7 | 0; - if ($3 >>> 0 < $4 >>> 0) { - $10 = $10 + 1 | 0 - } - $5 = $3; - $4 = $6 + $3 | 0; - $3 = $10; - $3 = $4 >>> 0 < $5 >>> 0 ? $3 + 1 | 0 : $3; - $5 = $4; - $6 = $3; - if (!($3 & 1048576)) { - break label$13 - } - $1 = $1 & 1 | (($2 & 1) << 31 | $1 >>> 1); - $2 = $5 << 31 | $2 >>> 1; - $12 = $12 + 1 | 0; - $5 = ($6 & 1) << 31 | $5 >>> 1; - $6 = $6 >>> 1 | 0; - } - $7 = 0; - $9 = $16 & -2147483648; - if (($12 | 0) >= 32767) { - $8 = $9 | 2147418112; - $5 = 0; - $6 = 0; - break label$1; - } - $4 = 0; - label$17 : { - if (($12 | 0) > 0) { - $4 = $12; - break label$17; - } - __ashlti3($11 + 16 | 0, $1, $2, $5, $6, $12 + 127 | 0); - __lshrti3($11, $1, $2, $5, $6, 1 - $12 | 0); - $1 = HEAP32[$11 >> 2] | ((HEAP32[$11 + 16 >> 2] | HEAP32[$11 + 24 >> 2]) != 0 | (HEAP32[$11 + 20 >> 2] | HEAP32[$11 + 28 >> 2]) != 0); - $2 = HEAP32[$11 + 4 >> 2]; - $5 = HEAP32[$11 + 8 >> 2]; - $6 = HEAP32[$11 + 12 >> 2]; - } - $7 = $7 | (($6 & 7) << 29 | $5 >>> 3); - $4 = $9 | $6 >>> 3 & 65535 | $4 << 16; - $9 = $5 << 29; - $3 = 0; - $5 = $9; - $6 = ($2 & 7) << 29 | $1 >>> 3 | $3; - $9 = $4; - $3 = $2 >>> 3 | $5; - $10 = $3; - $4 = $1 & 7; - $1 = $4 >>> 0 > 4; - $2 = $1 + $6 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $10 = $10 + 1 | 0 - } - $1 = $2; - $2 = $10; - $3 = ($3 | 0) == ($2 | 0) & $1 >>> 0 < $6 >>> 0 | $2 >>> 0 < $3 >>> 0; - $5 = $3 + $7 | 0; - if ($5 >>> 0 < $3 >>> 0) { - $9 = $9 + 1 | 0 - } - $4 = ($4 | 0) == 4; - $3 = $4 ? $1 & 1 : 0; - $8 = $9; - $7 = $5; - $4 = 0; - $9 = $2 + $4 | 0; - $2 = $1 + $3 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $9 = $9 + 1 | 0 - } - $5 = $2; - $1 = $2; - $6 = $9; - $1 = ($4 | 0) == ($9 | 0) & $1 >>> 0 < $3 >>> 0 | $9 >>> 0 < $4 >>> 0; - $2 = $7 + $1 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $8 = $8 + 1 | 0 - } - $7 = $2; - } - HEAP32[$0 >> 2] = $5; - HEAP32[$0 + 4 >> 2] = $6; - HEAP32[$0 + 8 >> 2] = $7; - HEAP32[$0 + 12 >> 2] = $8; - global$0 = $11 + 112 | 0; - } - - function __extenddftf2($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $5 = global$0 - 16 | 0; - global$0 = $5; - wasm2js_scratch_store_f64(+$1); - $8 = wasm2js_scratch_load_i32(1) | 0; - $6 = wasm2js_scratch_load_i32(0) | 0; - $7 = $8 & 2147483647; - $2 = $7; - $4 = $2 + -1048576 | 0; - $3 = $6; - if ($3 >>> 0 < 0) { - $4 = $4 + 1 | 0 - } - label$1 : { - if (($4 | 0) == 2145386495 | $4 >>> 0 < 2145386495) { - $7 = $3 << 28; - $4 = ($2 & 15) << 28 | $3 >>> 4; - $2 = ($2 >>> 4 | 0) + 1006632960 | 0; - $3 = $4; - $2 = $3 >>> 0 < 0 ? $2 + 1 | 0 : $2; - break label$1; - } - if (($2 | 0) == 2146435072 & $3 >>> 0 >= 0 | $2 >>> 0 > 2146435072) { - $7 = $6 << 28; - $4 = $6; - $2 = $8; - $6 = $2 >>> 4 | 0; - $3 = ($2 & 15) << 28 | $4 >>> 4; - $2 = $6 | 2147418112; - break label$1; - } - if (!($2 | $3)) { - $7 = 0; - $3 = 0; - $2 = 0; - break label$1; - } - $4 = $2; - $2 = ($2 | 0) == 1 & $3 >>> 0 < 0 | $2 >>> 0 < 1 ? Math_clz32($6) + 32 | 0 : Math_clz32($2); - __ashlti3($5, $3, $4, 0, 0, $2 + 49 | 0); - $9 = HEAP32[$5 >> 2]; - $7 = HEAP32[$5 + 4 >> 2]; - $3 = HEAP32[$5 + 8 >> 2]; - $2 = HEAP32[$5 + 12 >> 2] ^ 65536 | 15372 - $2 << 16; - } - HEAP32[$0 >> 2] = $9; - HEAP32[$0 + 4 >> 2] = $7; - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $8 & -2147483648 | $2; - global$0 = $5 + 16 | 0; - } - - function __letf2($0, $1, $2, $3, $4, $5, $6, $7) { - var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0; - $9 = 1; - $8 = $3 & 2147483647; - $12 = $8; - $10 = $2; - label$1 : { - if (!$2 & ($8 | 0) == 2147418112 ? $0 | $1 : ($8 | 0) == 2147418112 & $2 >>> 0 > 0 | $8 >>> 0 > 2147418112) { - break label$1 - } - $11 = $7 & 2147483647; - $13 = $11; - $8 = $6; - if (!$6 & ($11 | 0) == 2147418112 ? $4 | $5 : ($11 | 0) == 2147418112 & $6 >>> 0 > 0 | $11 >>> 0 > 2147418112) { - break label$1 - } - if (!($0 | $4 | ($8 | $10) | ($1 | $5 | ($12 | $13)))) { - return 0 - } - $10 = $3 & $7; - if (($10 | 0) > 0 ? 1 : ($10 | 0) >= 0 ? (($2 & $6) >>> 0 < 0 ? 0 : 1) : 0) { - $9 = -1; - if (($2 | 0) == ($6 | 0) & ($3 | 0) == ($7 | 0) ? ($1 | 0) == ($5 | 0) & $0 >>> 0 < $4 >>> 0 | $1 >>> 0 < $5 >>> 0 : ($3 | 0) < ($7 | 0) ? 1 : ($3 | 0) <= ($7 | 0) ? ($2 >>> 0 >= $6 >>> 0 ? 0 : 1) : 0) { - break label$1 - } - return ($0 ^ $4 | $2 ^ $6) != 0 | ($1 ^ $5 | $3 ^ $7) != 0; - } - $9 = -1; - if (($2 | 0) == ($6 | 0) & ($3 | 0) == ($7 | 0) ? ($1 | 0) == ($5 | 0) & $0 >>> 0 > $4 >>> 0 | $1 >>> 0 > $5 >>> 0 : ($3 | 0) > ($7 | 0) ? 1 : ($3 | 0) >= ($7 | 0) ? ($2 >>> 0 <= $6 >>> 0 ? 0 : 1) : 0) { - break label$1 - } - $9 = ($0 ^ $4 | $2 ^ $6) != 0 | ($1 ^ $5 | $3 ^ $7) != 0; - } - return $9; - } - - function __getf2($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $7 = -1; - $5 = $3 & 2147483647; - $8 = $5; - $6 = $2; - label$1 : { - if (!$2 & ($5 | 0) == 2147418112 ? $0 | $1 : ($5 | 0) == 2147418112 & $2 >>> 0 > 0 | $5 >>> 0 > 2147418112) { - break label$1 - } - $5 = $4 & 2147483647; - $9 = $5; - if (($5 | 0) == 2147418112 ? 0 : $5 >>> 0 > 2147418112) { - break label$1 - } - if (!($0 | $6 | ($1 | ($8 | $9)))) { - return 0 - } - $6 = $3 & $4; - if (($6 | 0) > 0 ? 1 : ($6 | 0) >= 0 ? 1 : 0) { - if (!$2 & ($3 | 0) == ($4 | 0) ? !$1 & $0 >>> 0 < 0 | $1 >>> 0 < 0 : ($3 | 0) < ($4 | 0) ? 1 : ($3 | 0) <= ($4 | 0) ? ($2 >>> 0 >= 0 ? 0 : 1) : 0) { - break label$1 - } - return ($0 | $2) != 0 | ($1 | $3 ^ $4) != 0; - } - if (!$2 & ($3 | 0) == ($4 | 0) ? !$1 & $0 >>> 0 > 0 | $1 >>> 0 > 0 : ($3 | 0) > ($4 | 0) ? 1 : ($3 | 0) >= ($4 | 0) ? ($2 >>> 0 <= 0 ? 0 : 1) : 0) { - break label$1 - } - $7 = ($0 | $2) != 0 | ($1 | $3 ^ $4) != 0; - } - return $7; - } - - function copysignl($0, $1, $2, $3, $4, $5, $6, $7, $8) { - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $4 & 65535 | ($8 >>> 16 & 32768 | $4 >>> 16 & 32767) << 16; - } - - function __floatunsitf($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0; - $2 = global$0 - 16 | 0; - global$0 = $2; - $6 = $0; - $7 = $0; - label$1 : { - if (!$1) { - $1 = 0; - $3 = 0; - break label$1; - } - $3 = $1; - $1 = Math_clz32($1) ^ 31; - __ashlti3($2, $3, 0, 0, 0, 112 - $1 | 0); - $1 = (HEAP32[$2 + 12 >> 2] ^ 65536) + ($1 + 16383 << 16) | 0; - $4 = 0 + HEAP32[$2 + 8 >> 2] | 0; - if ($4 >>> 0 < $5 >>> 0) { - $1 = $1 + 1 | 0 - } - $5 = HEAP32[$2 + 4 >> 2]; - $3 = HEAP32[$2 >> 2]; - } - HEAP32[$7 >> 2] = $3; - HEAP32[$6 + 4 >> 2] = $5; - HEAP32[$0 + 8 >> 2] = $4; - HEAP32[$0 + 12 >> 2] = $1; - global$0 = $2 + 16 | 0; - } - - function __subtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0; - $9 = global$0 - 16 | 0; - global$0 = $9; - __addtf3($9, $1, $2, $3, $4, $5, $6, $7, $8 ^ -2147483648); - $1 = HEAP32[$9 + 4 >> 2]; - HEAP32[$0 >> 2] = HEAP32[$9 >> 2]; - HEAP32[$0 + 4 >> 2] = $1; - $1 = HEAP32[$9 + 12 >> 2]; - HEAP32[$0 + 8 >> 2] = HEAP32[$9 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $1; - global$0 = $9 + 16 | 0; - } - - function scalbnl($0, $1, $2, $3, $4, $5) { - var $6 = 0; - $6 = global$0 - 80 | 0; - global$0 = $6; - label$1 : { - if (($5 | 0) >= 16384) { - __multf3($6 + 32 | 0, $1, $2, $3, $4, 0, 0, 0, 2147352576); - $3 = HEAP32[$6 + 40 >> 2]; - $4 = HEAP32[$6 + 44 >> 2]; - $1 = HEAP32[$6 + 32 >> 2]; - $2 = HEAP32[$6 + 36 >> 2]; - if (($5 | 0) < 32767) { - $5 = $5 + -16383 | 0; - break label$1; - } - __multf3($6 + 16 | 0, $1, $2, $3, $4, 0, 0, 0, 2147352576); - $5 = (($5 | 0) < 49149 ? $5 : 49149) + -32766 | 0; - $3 = HEAP32[$6 + 24 >> 2]; - $4 = HEAP32[$6 + 28 >> 2]; - $1 = HEAP32[$6 + 16 >> 2]; - $2 = HEAP32[$6 + 20 >> 2]; - break label$1; - } - if (($5 | 0) > -16383) { - break label$1 - } - __multf3($6 - -64 | 0, $1, $2, $3, $4, 0, 0, 0, 65536); - $3 = HEAP32[$6 + 72 >> 2]; - $4 = HEAP32[$6 + 76 >> 2]; - $1 = HEAP32[$6 + 64 >> 2]; - $2 = HEAP32[$6 + 68 >> 2]; - if (($5 | 0) > -32765) { - $5 = $5 + 16382 | 0; - break label$1; - } - __multf3($6 + 48 | 0, $1, $2, $3, $4, 0, 0, 0, 65536); - $5 = (($5 | 0) > -49146 ? $5 : -49146) + 32764 | 0; - $3 = HEAP32[$6 + 56 >> 2]; - $4 = HEAP32[$6 + 60 >> 2]; - $1 = HEAP32[$6 + 48 >> 2]; - $2 = HEAP32[$6 + 52 >> 2]; - } - __multf3($6, $1, $2, $3, $4, 0, 0, 0, $5 + 16383 << 16); - $1 = HEAP32[$6 + 12 >> 2]; - HEAP32[$0 + 8 >> 2] = HEAP32[$6 + 8 >> 2]; - HEAP32[$0 + 12 >> 2] = $1; - $1 = HEAP32[$6 + 4 >> 2]; - HEAP32[$0 >> 2] = HEAP32[$6 >> 2]; - HEAP32[$0 + 4 >> 2] = $1; - global$0 = $6 + 80 | 0; - } - - function __multi3($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; - $5 = __wasm_i64_mul($1, $2, 0, 0); - $6 = i64toi32_i32$HIGH_BITS; - $7 = __wasm_i64_mul(0, 0, $3, $4); - $5 = $5 + $7 | 0; - $6 = i64toi32_i32$HIGH_BITS + $6 | 0; - $9 = __wasm_i64_mul($4, 0, $2, 0); - $8 = $5 + $9 | 0; - $5 = i64toi32_i32$HIGH_BITS + ($5 >>> 0 < $7 >>> 0 ? $6 + 1 | 0 : $6) | 0; - $6 = __wasm_i64_mul($3, 0, $1, 0); - $10 = i64toi32_i32$HIGH_BITS; - $7 = __wasm_i64_mul($2, 0, $3, 0); - $3 = $10 + $7 | 0; - $2 = $8 >>> 0 < $9 >>> 0 ? $5 + 1 | 0 : $5; - $5 = i64toi32_i32$HIGH_BITS; - $5 = $3 >>> 0 < $7 >>> 0 ? $5 + 1 | 0 : $5; - $8 = $5 + $8 | 0; - if ($8 >>> 0 < $5 >>> 0) { - $2 = $2 + 1 | 0 - } - $1 = __wasm_i64_mul($1, 0, $4, 0) + $3 | 0; - $4 = i64toi32_i32$HIGH_BITS; - $3 = $1 >>> 0 < $3 >>> 0 ? $4 + 1 | 0 : $4; - $4 = $8 + $3 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $2 = $2 + 1 | 0 - } - HEAP32[$0 + 8 >> 2] = $4; - HEAP32[$0 + 12 >> 2] = $2; - HEAP32[$0 >> 2] = $6; - HEAP32[$0 + 4 >> 2] = $1; - } - - function __divtf3($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0; - $13 = global$0 - 192 | 0; - global$0 = $13; - $29 = $7; - $32 = $8 & 65535; - $16 = $3; - $14 = $4 & 65535; - $28 = ($4 ^ $8) & -2147483648; - $17 = $8 >>> 16 & 32767; - label$1 : { - $19 = $4 >>> 16 & 32767; - label$2 : { - label$3 : { - if ($19 + -1 >>> 0 <= 32765) { - if ($17 + -1 >>> 0 < 32766) { - break label$3 - } - } - $10 = $4 & 2147483647; - $11 = $10; - $9 = $3; - if (!(!$9 & ($10 | 0) == 2147418112 ? !($1 | $2) : ($10 | 0) == 2147418112 & $9 >>> 0 < 0 | $10 >>> 0 < 2147418112)) { - $33 = $3; - $28 = $4 | 32768; - break label$2; - } - $10 = $8 & 2147483647; - $4 = $10; - $3 = $7; - if (!(!$3 & ($10 | 0) == 2147418112 ? !($5 | $6) : ($10 | 0) == 2147418112 & $3 >>> 0 < 0 | $10 >>> 0 < 2147418112)) { - $33 = $7; - $28 = $8 | 32768; - $1 = $5; - $2 = $6; - break label$2; - } - if (!($1 | $9 | ($11 ^ 2147418112 | $2))) { - if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { - $1 = 0; - $2 = 0; - $28 = 2147450880; - break label$2; - } - $28 = $28 | 2147418112; - $1 = 0; - $2 = 0; - break label$2; - } - if (!($3 | $5 | ($4 ^ 2147418112 | $6))) { - $1 = 0; - $2 = 0; - break label$2; - } - if (!($1 | $9 | ($2 | $11))) { - break label$1 - } - if (!($3 | $5 | ($4 | $6))) { - $28 = $28 | 2147418112; - $1 = 0; - $2 = 0; - break label$2; - } - $10 = 0; - if (($11 | 0) == 65535 | $11 >>> 0 < 65535) { - $8 = $1; - $3 = !($14 | $16); - $7 = $3 << 6; - $9 = Math_clz32($3 ? $1 : $16) + 32 | 0; - $1 = Math_clz32($3 ? $2 : $14); - $1 = $7 + (($1 | 0) == 32 ? $9 : $1) | 0; - __ashlti3($13 + 176 | 0, $8, $2, $16, $14, $1 + -15 | 0); - $10 = 16 - $1 | 0; - $16 = HEAP32[$13 + 184 >> 2]; - $14 = HEAP32[$13 + 188 >> 2]; - $2 = HEAP32[$13 + 180 >> 2]; - $1 = HEAP32[$13 + 176 >> 2]; - } - if ($4 >>> 0 > 65535) { - break label$3 - } - $3 = !($29 | $32); - $4 = $3 << 6; - $7 = Math_clz32($3 ? $5 : $29) + 32 | 0; - $3 = Math_clz32($3 ? $6 : $32); - $3 = $4 + (($3 | 0) == 32 ? $7 : $3) | 0; - __ashlti3($13 + 160 | 0, $5, $6, $29, $32, $3 + -15 | 0); - $10 = ($3 + $10 | 0) + -16 | 0; - $29 = HEAP32[$13 + 168 >> 2]; - $32 = HEAP32[$13 + 172 >> 2]; - $5 = HEAP32[$13 + 160 >> 2]; - $6 = HEAP32[$13 + 164 >> 2]; - } - $4 = $32 | 65536; - $31 = $4; - $38 = $29; - $3 = $29; - $12 = $4 << 15 | $3 >>> 17; - $3 = $3 << 15 | $6 >>> 17; - $7 = -102865788 - $3 | 0; - $4 = $12; - $9 = $4; - $8 = 1963258675 - ($9 + (4192101508 < $3 >>> 0) | 0) | 0; - __multi3($13 + 144 | 0, $3, $9, $7, $8); - $9 = HEAP32[$13 + 152 >> 2]; - __multi3($13 + 128 | 0, 0 - $9 | 0, 0 - (HEAP32[$13 + 156 >> 2] + (0 < $9 >>> 0) | 0) | 0, $7, $8); - $7 = HEAP32[$13 + 136 >> 2]; - $8 = $7 << 1 | HEAP32[$13 + 132 >> 2] >>> 31; - $7 = HEAP32[$13 + 140 >> 2] << 1 | $7 >>> 31; - __multi3($13 + 112 | 0, $8, $7, $3, $4); - $9 = $7; - $7 = HEAP32[$13 + 120 >> 2]; - __multi3($13 + 96 | 0, $8, $9, 0 - $7 | 0, 0 - (HEAP32[$13 + 124 >> 2] + (0 < $7 >>> 0) | 0) | 0); - $7 = HEAP32[$13 + 104 >> 2]; - $11 = HEAP32[$13 + 108 >> 2] << 1 | $7 >>> 31; - $8 = $7 << 1 | HEAP32[$13 + 100 >> 2] >>> 31; - __multi3($13 + 80 | 0, $8, $11, $3, $4); - $7 = HEAP32[$13 + 88 >> 2]; - __multi3($13 - -64 | 0, $8, $11, 0 - $7 | 0, 0 - (HEAP32[$13 + 92 >> 2] + (0 < $7 >>> 0) | 0) | 0); - $7 = HEAP32[$13 + 72 >> 2]; - $8 = $7 << 1 | HEAP32[$13 + 68 >> 2] >>> 31; - $7 = HEAP32[$13 + 76 >> 2] << 1 | $7 >>> 31; - __multi3($13 + 48 | 0, $8, $7, $3, $4); - $9 = $7; - $7 = HEAP32[$13 + 56 >> 2]; - __multi3($13 + 32 | 0, $8, $9, 0 - $7 | 0, 0 - (HEAP32[$13 + 60 >> 2] + (0 < $7 >>> 0) | 0) | 0); - $7 = HEAP32[$13 + 40 >> 2]; - $11 = HEAP32[$13 + 44 >> 2] << 1 | $7 >>> 31; - $8 = $7 << 1 | HEAP32[$13 + 36 >> 2] >>> 31; - __multi3($13 + 16 | 0, $8, $11, $3, $4); - $7 = HEAP32[$13 + 24 >> 2]; - __multi3($13, $8, $11, 0 - $7 | 0, 0 - (HEAP32[$13 + 28 >> 2] + (0 < $7 >>> 0) | 0) | 0); - $34 = ($19 - $17 | 0) + $10 | 0; - $7 = HEAP32[$13 + 8 >> 2]; - $9 = HEAP32[$13 + 12 >> 2] << 1 | $7 >>> 31; - $8 = $7 << 1; - $10 = $9 + -1 | 0; - $8 = (HEAP32[$13 + 4 >> 2] >>> 31 | $8) + -1 | 0; - if (($8 | 0) != -1) { - $10 = $10 + 1 | 0 - } - $7 = $8; - $9 = 0; - $21 = $9; - $20 = $4; - $11 = 0; - $12 = __wasm_i64_mul($7, $9, $4, $11); - $4 = i64toi32_i32$HIGH_BITS; - $19 = $4; - $22 = $10; - $17 = 0; - $9 = $3; - $7 = __wasm_i64_mul($10, $17, $9, 0); - $3 = $7 + $12 | 0; - $10 = i64toi32_i32$HIGH_BITS + $4 | 0; - $10 = $3 >>> 0 < $7 >>> 0 ? $10 + 1 | 0 : $10; - $7 = $3; - $3 = $10; - $15 = __wasm_i64_mul($8, $21, $9, $15); - $4 = 0 + $15 | 0; - $10 = $7; - $9 = $10 + i64toi32_i32$HIGH_BITS | 0; - $9 = $4 >>> 0 < $15 >>> 0 ? $9 + 1 | 0 : $9; - $15 = $4; - $4 = $9; - $9 = ($10 | 0) == ($9 | 0) & $15 >>> 0 < $23 >>> 0 | $9 >>> 0 < $10 >>> 0; - $10 = ($3 | 0) == ($19 | 0) & $10 >>> 0 < $12 >>> 0 | $3 >>> 0 < $19 >>> 0; - $7 = $3; - $3 = __wasm_i64_mul($22, $17, $20, $11) + $3 | 0; - $11 = $10 + i64toi32_i32$HIGH_BITS | 0; - $11 = $3 >>> 0 < $7 >>> 0 ? $11 + 1 | 0 : $11; - $7 = $3; - $3 = $9 + $3 | 0; - $9 = $11; - $26 = $3; - $7 = $3 >>> 0 < $7 >>> 0 ? $9 + 1 | 0 : $9; - $3 = $6; - $24 = ($3 & 131071) << 15 | $5 >>> 17; - $20 = __wasm_i64_mul($8, $21, $24, 0); - $3 = i64toi32_i32$HIGH_BITS; - $23 = $3; - $10 = $5; - $18 = $10 << 15 & -32768; - $11 = __wasm_i64_mul($22, $17, $18, 0); - $9 = $11 + $20 | 0; - $10 = i64toi32_i32$HIGH_BITS + $3 | 0; - $10 = $9 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; - $3 = $10; - $25 = __wasm_i64_mul($8, $21, $18, $25); - $18 = 0 + $25 | 0; - $10 = $9 + i64toi32_i32$HIGH_BITS | 0; - $10 = $18 >>> 0 < $25 >>> 0 ? $10 + 1 | 0 : $10; - $10 = ($9 | 0) == ($10 | 0) & $18 >>> 0 < $30 >>> 0 | $10 >>> 0 < $9 >>> 0; - $9 = ($3 | 0) == ($23 | 0) & $9 >>> 0 < $20 >>> 0 | $3 >>> 0 < $23 >>> 0; - $12 = $3; - $3 = __wasm_i64_mul($22, $17, $24, $27) + $3 | 0; - $11 = $9 + i64toi32_i32$HIGH_BITS | 0; - $11 = $3 >>> 0 < $12 >>> 0 ? $11 + 1 | 0 : $11; - $9 = $3; - $3 = $10 + $9 | 0; - $12 = $3 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; - $10 = $3; - $3 = $15 + $3 | 0; - $9 = $12 + $4 | 0; - $9 = $3 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; - $19 = $3; - $10 = $7; - $20 = $9; - $3 = ($4 | 0) == ($9 | 0) & $3 >>> 0 < $15 >>> 0 | $9 >>> 0 < $4 >>> 0; - $4 = $3 + $26 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $10 = $10 + 1 | 0 - } - $9 = $10; - $3 = ($19 | 0) != 0 | ($20 | 0) != 0; - $4 = $4 + $3 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $9 = $9 + 1 | 0 - } - $10 = $4; - $4 = 0 - $10 | 0; - $15 = 0; - $7 = __wasm_i64_mul($4, $15, $8, $21); - $3 = i64toi32_i32$HIGH_BITS; - $23 = $3; - $18 = __wasm_i64_mul($22, $17, $4, $15); - $4 = i64toi32_i32$HIGH_BITS; - $26 = $4; - $24 = 0 - ((0 < $10 >>> 0) + $9 | 0) | 0; - $9 = 0; - $15 = __wasm_i64_mul($8, $21, $24, $9); - $12 = $15 + $18 | 0; - $10 = i64toi32_i32$HIGH_BITS + $4 | 0; - $10 = $12 >>> 0 < $15 >>> 0 ? $10 + 1 | 0 : $10; - $4 = $12; - $15 = 0 + $7 | 0; - $11 = $3 + $4 | 0; - $11 = $15 >>> 0 < $27 >>> 0 ? $11 + 1 | 0 : $11; - $12 = $15; - $3 = $11; - $11 = ($23 | 0) == ($3 | 0) & $12 >>> 0 < $7 >>> 0 | $3 >>> 0 < $23 >>> 0; - $12 = ($10 | 0) == ($26 | 0) & $4 >>> 0 < $18 >>> 0 | $10 >>> 0 < $26 >>> 0; - $4 = __wasm_i64_mul($22, $17, $24, $9) + $10 | 0; - $9 = $12 + i64toi32_i32$HIGH_BITS | 0; - $9 = $4 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9; - $7 = $4; - $4 = $11 + $4 | 0; - if ($4 >>> 0 < $7 >>> 0) { - $9 = $9 + 1 | 0 - } - $24 = $4; - $7 = $9; - $4 = 0 - $19 | 0; - $27 = 0 - ((0 < $19 >>> 0) + $20 | 0) | 0; - $19 = 0; - $26 = __wasm_i64_mul($27, $19, $8, $21); - $18 = i64toi32_i32$HIGH_BITS; - $20 = $4; - $25 = 0; - $9 = __wasm_i64_mul($4, $25, $22, $17); - $4 = $9 + $26 | 0; - $10 = i64toi32_i32$HIGH_BITS + $18 | 0; - $11 = $4; - $4 = $4 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; - $20 = __wasm_i64_mul($8, $21, $20, $25); - $8 = 0 + $20 | 0; - $9 = $11; - $10 = $9 + i64toi32_i32$HIGH_BITS | 0; - $10 = $8 >>> 0 < $20 >>> 0 ? $10 + 1 | 0 : $10; - $10 = ($9 | 0) == ($10 | 0) & $8 >>> 0 < $30 >>> 0 | $10 >>> 0 < $9 >>> 0; - $9 = ($4 | 0) == ($18 | 0) & $9 >>> 0 < $26 >>> 0 | $4 >>> 0 < $18 >>> 0; - $8 = $4; - $4 = __wasm_i64_mul($27, $19, $22, $17) + $4 | 0; - $12 = $9 + i64toi32_i32$HIGH_BITS | 0; - $12 = $4 >>> 0 < $8 >>> 0 ? $12 + 1 | 0 : $12; - $8 = $4; - $4 = $10 + $4 | 0; - $9 = $12; - $9 = $4 >>> 0 < $8 >>> 0 ? $9 + 1 | 0 : $9; - $8 = $4; - $4 = $15 + $4 | 0; - $9 = $9 + $3 | 0; - $9 = $4 >>> 0 < $8 >>> 0 ? $9 + 1 | 0 : $9; - $8 = $4; - $10 = $7; - $4 = $9; - $3 = ($3 | 0) == ($9 | 0) & $8 >>> 0 < $15 >>> 0 | $9 >>> 0 < $3 >>> 0; - $7 = $3 + $24 | 0; - if ($7 >>> 0 < $3 >>> 0) { - $10 = $10 + 1 | 0 - } - $3 = $7; - $9 = $10; - $12 = $3; - $11 = $4 + -1 | 0; - $3 = $8 + -2 | 0; - if ($3 >>> 0 < 4294967294) { - $11 = $11 + 1 | 0 - } - $7 = $3; - $10 = $3; - $3 = $11; - $4 = ($4 | 0) == ($3 | 0) & $10 >>> 0 < $8 >>> 0 | $3 >>> 0 < $4 >>> 0; - $8 = $12 + $4 | 0; - if ($8 >>> 0 < $4 >>> 0) { - $9 = $9 + 1 | 0 - } - $4 = $8 + -1 | 0; - $10 = $9 + -1 | 0; - $10 = ($4 | 0) != -1 ? $10 + 1 | 0 : $10; - $8 = 0; - $22 = $8; - $17 = $4; - $9 = $16; - $18 = $9 << 2 | $2 >>> 30; - $24 = 0; - $12 = __wasm_i64_mul($4, $8, $18, $24); - $8 = i64toi32_i32$HIGH_BITS; - $15 = $8; - $11 = $8; - $8 = $2; - $27 = ($8 & 1073741823) << 2 | $1 >>> 30; - $25 = $10; - $8 = 0; - $9 = __wasm_i64_mul($27, 0, $10, $8); - $4 = $9 + $12 | 0; - $11 = i64toi32_i32$HIGH_BITS + $11 | 0; - $11 = $4 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; - $9 = $4; - $20 = $11; - $23 = ($15 | 0) == ($11 | 0) & $9 >>> 0 < $12 >>> 0 | $11 >>> 0 < $15 >>> 0; - $12 = $11; - $11 = 0; - $15 = $11; - $10 = 0; - $26 = $3; - $30 = (($14 & 1073741823) << 2 | $16 >>> 30) & -262145 | 262144; - $4 = __wasm_i64_mul($3, $11, $30, 0); - $3 = $4 + $9 | 0; - $12 = i64toi32_i32$HIGH_BITS + $12 | 0; - $12 = $3 >>> 0 < $4 >>> 0 ? $12 + 1 | 0 : $12; - $16 = $3; - $4 = $12; - $3 = ($20 | 0) == ($4 | 0) & $3 >>> 0 < $9 >>> 0 | $4 >>> 0 < $20 >>> 0; - $9 = $3 + $23 | 0; - if ($9 >>> 0 < $3 >>> 0) { - $10 = 1 - } - $11 = __wasm_i64_mul($25, $8, $30, $35); - $3 = $11 + $9 | 0; - $9 = i64toi32_i32$HIGH_BITS + $10 | 0; - $10 = $3 >>> 0 < $11 >>> 0 ? $9 + 1 | 0 : $9; - $11 = __wasm_i64_mul($17, $22, $30, $35); - $9 = i64toi32_i32$HIGH_BITS; - $2 = $3; - $14 = __wasm_i64_mul($18, $24, $25, $8); - $3 = $14 + $11 | 0; - $12 = i64toi32_i32$HIGH_BITS + $9 | 0; - $12 = $3 >>> 0 < $14 >>> 0 ? $12 + 1 | 0 : $12; - $14 = $3; - $3 = $12; - $12 = ($9 | 0) == ($3 | 0) & $14 >>> 0 < $11 >>> 0 | $3 >>> 0 < $9 >>> 0; - $11 = $2 + $3 | 0; - $10 = $10 + $12 | 0; - $9 = $11; - $12 = $9 >>> 0 < $3 >>> 0 ? $10 + 1 | 0 : $10; - $2 = $9; - $11 = $4 + $14 | 0; - $10 = 0; - $3 = $10 + $16 | 0; - if ($3 >>> 0 < $10 >>> 0) { - $11 = $11 + 1 | 0 - } - $14 = $3; - $9 = $3; - $3 = $11; - $4 = ($4 | 0) == ($3 | 0) & $9 >>> 0 < $16 >>> 0 | $3 >>> 0 < $4 >>> 0; - $9 = $2 + $4 | 0; - if ($9 >>> 0 < $4 >>> 0) { - $12 = $12 + 1 | 0 - } - $39 = $9; - $4 = $14; - $10 = $3; - $16 = __wasm_i64_mul($27, $19, $26, $15); - $11 = i64toi32_i32$HIGH_BITS; - $20 = $7; - $23 = __wasm_i64_mul($7, 0, $18, $24); - $7 = $23 + $16 | 0; - $9 = i64toi32_i32$HIGH_BITS + $11 | 0; - $9 = $7 >>> 0 < $23 >>> 0 ? $9 + 1 | 0 : $9; - $21 = $7; - $7 = $9; - $16 = ($11 | 0) == ($9 | 0) & $21 >>> 0 < $16 >>> 0 | $9 >>> 0 < $11 >>> 0; - $11 = $9; - $40 = $4; - $9 = 0; - $41 = $16; - $36 = $1 << 2 & -4; - $2 = 0; - $16 = __wasm_i64_mul($17, $22, $36, $2); - $4 = $16 + $21 | 0; - $11 = i64toi32_i32$HIGH_BITS + $11 | 0; - $11 = $4 >>> 0 < $16 >>> 0 ? $11 + 1 | 0 : $11; - $23 = $4; - $16 = $4; - $4 = $11; - $7 = ($7 | 0) == ($4 | 0) & $16 >>> 0 < $21 >>> 0 | $4 >>> 0 < $7 >>> 0; - $11 = $41 + $7 | 0; - if ($11 >>> 0 < $7 >>> 0) { - $9 = 1 - } - $7 = $40 + $11 | 0; - $10 = $9 + $10 | 0; - $10 = $7 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; - $16 = $7; - $11 = $12; - $7 = $10; - $3 = ($3 | 0) == ($10 | 0) & $16 >>> 0 < $14 >>> 0 | $10 >>> 0 < $3 >>> 0; - $9 = $3 + $39 | 0; - if ($9 >>> 0 < $3 >>> 0) { - $11 = $11 + 1 | 0 - } - $40 = $9; - $14 = $16; - $21 = $7; - $39 = __wasm_i64_mul($25, $8, $36, $2); - $25 = i64toi32_i32$HIGH_BITS; - $8 = __wasm_i64_mul($30, $35, $20, $37); - $3 = $8 + $39 | 0; - $12 = i64toi32_i32$HIGH_BITS + $25 | 0; - $12 = $3 >>> 0 < $8 >>> 0 ? $12 + 1 | 0 : $12; - $30 = $3; - $9 = __wasm_i64_mul($18, $24, $26, $15); - $3 = $3 + $9 | 0; - $8 = $12; - $10 = $8 + i64toi32_i32$HIGH_BITS | 0; - $10 = $3 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; - $18 = $3; - $12 = __wasm_i64_mul($17, $22, $27, $19); - $3 = $3 + $12 | 0; - $9 = i64toi32_i32$HIGH_BITS + $10 | 0; - $17 = $3; - $9 = $3 >>> 0 < $12 >>> 0 ? $9 + 1 | 0 : $9; - $22 = 0; - $12 = $11; - $3 = $9; - $9 = ($9 | 0) == ($10 | 0) & $17 >>> 0 < $18 >>> 0 | $9 >>> 0 < $10 >>> 0; - $11 = ($8 | 0) == ($25 | 0) & $30 >>> 0 < $39 >>> 0 | $8 >>> 0 < $25 >>> 0; - $8 = ($8 | 0) == ($10 | 0) & $18 >>> 0 < $30 >>> 0 | $10 >>> 0 < $8 >>> 0; - $10 = $11 + $8 | 0; - $10 >>> 0 < $8 >>> 0; - $8 = $9 + $10 | 0; - $10 = $8; - $9 = $3 | 0; - $8 = $9 + $14 | 0; - $10 = ($10 | $22) + $21 | 0; - $10 = $8 >>> 0 < $9 >>> 0 ? $10 + 1 | 0 : $10; - $21 = $8; - $14 = $10; - $7 = ($7 | 0) == ($10 | 0) & $8 >>> 0 < $16 >>> 0 | $10 >>> 0 < $7 >>> 0; - $8 = $7 + $40 | 0; - if ($8 >>> 0 < $7 >>> 0) { - $12 = $12 + 1 | 0 - } - $24 = $8; - $8 = $12; - $12 = $21; - $16 = $14; - $22 = $23; - $26 = __wasm_i64_mul($26, $15, $36, $2); - $15 = i64toi32_i32$HIGH_BITS; - $9 = __wasm_i64_mul($27, $19, $20, $37); - $7 = $9 + $26 | 0; - $11 = i64toi32_i32$HIGH_BITS + $15 | 0; - $11 = $7 >>> 0 < $9 >>> 0 ? $11 + 1 | 0 : $11; - $10 = $11; - $19 = $10; - $11 = 0; - $9 = ($10 | 0) == ($15 | 0) & $7 >>> 0 < $26 >>> 0 | $10 >>> 0 < $15 >>> 0; - $7 = $10 + $22 | 0; - $10 = ($9 | $11) + $4 | 0; - $10 = $7 >>> 0 < $19 >>> 0 ? $10 + 1 | 0 : $10; - $19 = $7; - $9 = $7; - $7 = $10; - $9 = ($4 | 0) == ($10 | 0) & $9 >>> 0 < $22 >>> 0 | $10 >>> 0 < $4 >>> 0; - $23 = $12; - $4 = $9; - $9 = $10 + $17 | 0; - $12 = 0; - $3 = $12 + $19 | 0; - if ($3 >>> 0 < $12 >>> 0) { - $9 = $9 + 1 | 0 - } - $3 = ($7 | 0) == ($9 | 0) & $3 >>> 0 < $19 >>> 0 | $9 >>> 0 < $7 >>> 0; - $4 = $4 + $3 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $11 = 1 - } - $3 = $23 + $4 | 0; - $12 = $11 + $16 | 0; - $7 = $3; - $9 = $8; - $12 = $3 >>> 0 < $4 >>> 0 ? $12 + 1 | 0 : $12; - $8 = $12; - $3 = ($14 | 0) == ($8 | 0) & $3 >>> 0 < $21 >>> 0 | $8 >>> 0 < $14 >>> 0; - $4 = $3 + $24 | 0; - if ($4 >>> 0 < $3 >>> 0) { - $9 = $9 + 1 | 0 - } - $3 = $4; - $4 = $9; - label$12 : { - if (($9 | 0) == 131071 | $9 >>> 0 < 131071) { - $22 = 0; - $14 = $5; - $18 = 0; - $10 = __wasm_i64_mul($7, $22, $14, $18); - $11 = i64toi32_i32$HIGH_BITS; - $9 = $1 << 17; - $1 = 0; - $2 = ($10 | 0) != 0 | ($11 | 0) != 0; - $16 = $1 - $2 | 0; - $30 = $9 - ($1 >>> 0 < $2 >>> 0) | 0; - $19 = 0 - $10 | 0; - $15 = 0 - ((0 < $10 >>> 0) + $11 | 0) | 0; - $2 = 0; - $24 = __wasm_i64_mul($8, $2, $14, $18); - $1 = i64toi32_i32$HIGH_BITS; - $27 = $1; - $17 = 0; - $10 = __wasm_i64_mul($7, $22, $6, $17); - $9 = $10 + $24 | 0; - $11 = i64toi32_i32$HIGH_BITS + $1 | 0; - $11 = $9 >>> 0 < $10 >>> 0 ? $11 + 1 | 0 : $11; - $1 = $9; - $10 = $9; - $20 = 0; - $9 = $20; - $23 = $10; - $9 = ($10 | 0) == ($15 | 0) & $19 >>> 0 < $9 >>> 0 | $15 >>> 0 < $10 >>> 0; - $21 = $16 - $9 | 0; - $30 = $30 - ($16 >>> 0 < $9 >>> 0) | 0; - $9 = __wasm_i64_mul($3, 0, $14, $18); - $10 = i64toi32_i32$HIGH_BITS; - $14 = __wasm_i64_mul($7, $22, $29, 0); - $9 = $14 + $9 | 0; - $12 = i64toi32_i32$HIGH_BITS + $10 | 0; - $12 = $9 >>> 0 < $14 >>> 0 ? $12 + 1 | 0 : $12; - $14 = __wasm_i64_mul($8, $2, $6, $17); - $9 = $14 + $9 | 0; - $10 = i64toi32_i32$HIGH_BITS + $12 | 0; - $10 = $9 >>> 0 < $14 >>> 0 ? $10 + 1 | 0 : $10; - $12 = $10; - $10 = ($11 | 0) == ($27 | 0) & $1 >>> 0 < $24 >>> 0 | $11 >>> 0 < $27 >>> 0; - $1 = $11 + $9 | 0; - $10 = $10 + $12 | 0; - $10 = $1 >>> 0 < $11 >>> 0 ? $10 + 1 | 0 : $10; - $11 = $1; - $1 = $10; - $9 = __wasm_i64_mul($7, $8, $31, 0); - $14 = i64toi32_i32$HIGH_BITS; - $16 = $11; - $11 = __wasm_i64_mul($5, $6, $4, 0); - $10 = $11 + $9 | 0; - $9 = i64toi32_i32$HIGH_BITS + $14 | 0; - $9 = $10 >>> 0 < $11 >>> 0 ? $9 + 1 | 0 : $9; - $12 = __wasm_i64_mul($3, $4, $6, $17); - $11 = $12 + $10 | 0; - $9 = __wasm_i64_mul($8, $2, $29, $32); - $2 = $9 + $11 | 0; - $9 = $2; - $10 = 0; - $2 = $16 + $10 | 0; - $9 = $1 + $9 | 0; - $1 = $2; - $16 = $21 - $1 | 0; - $2 = $30 - (($21 >>> 0 < $1 >>> 0) + ($1 >>> 0 < $10 >>> 0 ? $9 + 1 | 0 : $9) | 0) | 0; - $34 = $34 + -1 | 0; - $29 = $19 - $20 | 0; - $1 = $15 - (($19 >>> 0 < $20 >>> 0) + $23 | 0) | 0; - break label$12; - } - $17 = $8 >>> 1 | 0; - $11 = 0; - $12 = $1 << 16; - $10 = $3 << 31; - $7 = ($8 & 1) << 31 | $7 >>> 1; - $8 = $8 >>> 1 | $10; - $27 = 0; - $25 = 0; - $1 = __wasm_i64_mul($7, $27, $5, $25); - $9 = i64toi32_i32$HIGH_BITS; - $10 = $9; - $9 = ($1 | 0) != 0 | ($9 | 0) != 0; - $14 = $2 - $9 | 0; - $37 = $12 - ($2 >>> 0 < $9 >>> 0) | 0; - $21 = 0 - $1 | 0; - $22 = 0 - ((0 < $1 >>> 0) + $10 | 0) | 0; - $12 = $22; - $15 = 0; - $20 = __wasm_i64_mul($7, $27, $6, $15); - $1 = i64toi32_i32$HIGH_BITS; - $35 = $1; - $23 = $17 | $3 << 31; - $36 = $4 << 31 | $3 >>> 1 | $11; - $10 = $23; - $17 = __wasm_i64_mul($10, 0, $5, $25); - $2 = $17 + $20 | 0; - $9 = i64toi32_i32$HIGH_BITS + $1 | 0; - $9 = $2 >>> 0 < $17 >>> 0 ? $9 + 1 | 0 : $9; - $1 = $9; - $9 = $2; - $26 = $9; - $18 = 0; - $9 = ($9 | 0) == ($12 | 0) & $21 >>> 0 < $18 >>> 0 | $12 >>> 0 < $9 >>> 0; - $24 = $14 - $9 | 0; - $37 = $37 - ($14 >>> 0 < $9 >>> 0) | 0; - $10 = __wasm_i64_mul($6, $15, $10, $11); - $11 = i64toi32_i32$HIGH_BITS; - $9 = $4; - $12 = $9 >>> 1 | 0; - $17 = ($9 & 1) << 31 | $3 >>> 1; - $14 = $12; - $12 = __wasm_i64_mul($17, 0, $5, $25); - $9 = $12 + $10 | 0; - $10 = i64toi32_i32$HIGH_BITS + $11 | 0; - $10 = $9 >>> 0 < $12 >>> 0 ? $10 + 1 | 0 : $10; - $12 = __wasm_i64_mul($7, $27, $29, 0); - $11 = $12 + $9 | 0; - $9 = i64toi32_i32$HIGH_BITS + $10 | 0; - $10 = $11; - $11 = $10 >>> 0 < $12 >>> 0 ? $9 + 1 | 0 : $9; - $9 = ($1 | 0) == ($35 | 0) & $2 >>> 0 < $20 >>> 0 | $1 >>> 0 < $35 >>> 0; - $2 = $1; - $1 = $1 + $10 | 0; - $11 = $9 + $11 | 0; - $9 = $1; - $1 = $9 >>> 0 < $2 >>> 0 ? $11 + 1 | 0 : $11; - $2 = __wasm_i64_mul($7, $8, $31, 0); - $10 = i64toi32_i32$HIGH_BITS; - $11 = $9; - $3 = __wasm_i64_mul($5, $6, $4 >>> 1 | 0, 0); - $2 = $3 + $2 | 0; - $9 = i64toi32_i32$HIGH_BITS + $10 | 0; - $9 = $2 >>> 0 < $3 >>> 0 ? $9 + 1 | 0 : $9; - $3 = __wasm_i64_mul($6, $15, $17, $14); - $2 = $3 + $2 | 0; - $9 = i64toi32_i32$HIGH_BITS + $9 | 0; - $3 = __wasm_i64_mul($23, $36, $29, $32); - $2 = $3 + $2 | 0; - $9 = $2; - $3 = 0; - $2 = $11 + $3 | 0; - $10 = $1 + $9 | 0; - $1 = $2; - $16 = $24 - $1 | 0; - $2 = $37 - (($24 >>> 0 < $1 >>> 0) + ($1 >>> 0 < $3 >>> 0 ? $10 + 1 | 0 : $10) | 0) | 0; - $3 = $17; - $4 = $14; - $29 = $21 - $18 | 0; - $1 = $22 - (($21 >>> 0 < $18 >>> 0) + $26 | 0) | 0; - } - if (($34 | 0) >= 16384) { - $28 = $28 | 2147418112; - $1 = 0; - $2 = 0; - break label$2; - } - $11 = $34 + 16383 | 0; - if (($34 | 0) <= -16383) { - label$16 : { - if ($11) { - break label$16 - } - $11 = $8; - $14 = $29; - $12 = $1 << 1 | $14 >>> 31; - $9 = $14 << 1; - $6 = ($6 | 0) == ($12 | 0) & $9 >>> 0 > $5 >>> 0 | $12 >>> 0 > $6 >>> 0; - $9 = $4 & 65535; - $5 = $16; - $12 = $2 << 1 | $5 >>> 31; - $2 = $5 << 1 | $1 >>> 31; - $4 = $2; - $1 = $12; - $1 = ($4 | 0) == ($38 | 0) & ($1 | 0) == ($31 | 0) ? $6 : ($31 | 0) == ($1 | 0) & $4 >>> 0 > $38 >>> 0 | $1 >>> 0 > $31 >>> 0; - $2 = $1 + $7 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $11 = $11 + 1 | 0 - } - $1 = $2; - $4 = $1; - $2 = $11; - $4 = $3 + (($8 | 0) == ($11 | 0) & $4 >>> 0 < $7 >>> 0 | $11 >>> 0 < $8 >>> 0) | 0; - if ($4 >>> 0 < $3 >>> 0) { - $9 = $9 + 1 | 0 - } - $3 = $9; - if (!($9 & 65536)) { - break label$16 - } - $33 = $4 | $33; - $28 = $3 | $28; - break label$2; - } - $1 = 0; - $2 = 0; - break label$2; - } - $10 = $8; - $4 = $4 & 65535; - $14 = $29; - $9 = $1 << 1 | $14 >>> 31; - $14 = $14 << 1; - $6 = ($6 | 0) == ($9 | 0) & $14 >>> 0 >= $5 >>> 0 | $9 >>> 0 > $6 >>> 0; - $5 = $16; - $9 = $2 << 1 | $5 >>> 31; - $2 = $5 << 1 | $1 >>> 31; - $1 = ($2 | 0) == ($38 | 0) & ($9 | 0) == ($31 | 0) ? $6 : ($31 | 0) == ($9 | 0) & $2 >>> 0 >= $38 >>> 0 | $9 >>> 0 > $31 >>> 0; - $2 = $1 + $7 | 0; - if ($2 >>> 0 < $1 >>> 0) { - $10 = $10 + 1 | 0 - } - $1 = $2; - $2 = $10; - $5 = $3; - $3 = (($8 | 0) == ($10 | 0) & $1 >>> 0 < $7 >>> 0 | $10 >>> 0 < $8 >>> 0) + $3 | 0; - $10 = $11 << 16 | $4; - $33 = $3 | $33; - $28 = $28 | ($3 >>> 0 < $5 >>> 0 ? $10 + 1 | 0 : $10); - } - HEAP32[$0 >> 2] = $1; - HEAP32[$0 + 4 >> 2] = $2; - HEAP32[$0 + 8 >> 2] = $33; - HEAP32[$0 + 12 >> 2] = $28; - global$0 = $13 + 192 | 0; - return; - } - HEAP32[$0 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - $1 = !($3 | $5 | ($4 | $6)); - HEAP32[$0 + 8 >> 2] = $1 ? 0 : $33; - HEAP32[$0 + 12 >> 2] = $1 ? 2147450880 : $28; - global$0 = $13 + 192 | 0; - } - - function __fpclassifyl($0, $1, $2, $3) { - var $4 = 0, $5 = 0; - $5 = $3 & 65535; - $3 = $3 >>> 16 & 32767; - label$1 : { - if (($3 | 0) != 32767) { - $4 = 4; - if ($3) { - break label$1 - } - return $0 | $2 | ($1 | $5) ? 3 : 2; - } - $4 = !($0 | $2 | ($1 | $5)); - } - return $4; - } - - function fmodl($0, $1, $2, $3, $4, $5, $6, $7, $8) { - var $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0; - $9 = global$0 - 128 | 0; - global$0 = $9; - label$1 : { - label$2 : { - label$3 : { - if (!__letf2($5, $6, $7, $8, 0, 0, 0, 0)) { - break label$3 - } - $10 = __fpclassifyl($5, $6, $7, $8); - $19 = $4 >>> 16 | 0; - $14 = $19 & 32767; - if (($14 | 0) == 32767) { - break label$3 - } - if ($10) { - break label$2 - } - } - __multf3($9 + 16 | 0, $1, $2, $3, $4, $5, $6, $7, $8); - $4 = HEAP32[$9 + 16 >> 2]; - $3 = HEAP32[$9 + 20 >> 2]; - $2 = HEAP32[$9 + 24 >> 2]; - $1 = HEAP32[$9 + 28 >> 2]; - __divtf3($9, $4, $3, $2, $1, $4, $3, $2, $1); - $3 = HEAP32[$9 + 8 >> 2]; - $4 = HEAP32[$9 + 12 >> 2]; - $7 = HEAP32[$9 >> 2]; - $8 = HEAP32[$9 + 4 >> 2]; - break label$1; - } - $11 = $4 & 65535 | $14 << 16; - $12 = $11; - $13 = $3; - $15 = $7; - $18 = $8 >>> 16 & 32767; - $10 = $8 & 65535 | $18 << 16; - if ((__letf2($1, $2, $13, $12, $5, $6, $7, $10) | 0) <= 0) { - if (__letf2($1, $2, $13, $12, $5, $6, $15, $10)) { - $7 = $1; - $8 = $2; - break label$1; - } - __multf3($9 + 112 | 0, $1, $2, $3, $4, 0, 0, 0, 0); - $3 = HEAP32[$9 + 120 >> 2]; - $4 = HEAP32[$9 + 124 >> 2]; - $7 = HEAP32[$9 + 112 >> 2]; - $8 = HEAP32[$9 + 116 >> 2]; - break label$1; - } - if ($14) { - $8 = $2; - $7 = $1; - } else { - __multf3($9 + 96 | 0, $1, $2, $13, $12, 0, 0, 0, 1081540608); - $7 = HEAP32[$9 + 108 >> 2]; - $12 = $7; - $13 = HEAP32[$9 + 104 >> 2]; - $14 = ($7 >>> 16 | 0) + -120 | 0; - $8 = HEAP32[$9 + 100 >> 2]; - $7 = HEAP32[$9 + 96 >> 2]; - } - if (!$18) { - __multf3($9 + 80 | 0, $5, $6, $15, $10, 0, 0, 0, 1081540608); - $5 = HEAP32[$9 + 92 >> 2]; - $10 = $5; - $15 = HEAP32[$9 + 88 >> 2]; - $18 = ($10 >>> 16 | 0) + -120 | 0; - $6 = HEAP32[$9 + 84 >> 2]; - $5 = HEAP32[$9 + 80 >> 2]; - } - $21 = $15; - $11 = $15; - $15 = $13 - $11 | 0; - $12 = $12 & 65535 | 65536; - $20 = $10 & 65535 | 65536; - $10 = ($6 | 0) == ($8 | 0) & $7 >>> 0 < $5 >>> 0 | $8 >>> 0 < $6 >>> 0; - $11 = ($12 - ($20 + ($13 >>> 0 < $11 >>> 0) | 0) | 0) - ($15 >>> 0 < $10 >>> 0) | 0; - $17 = $15 - $10 | 0; - $16 = ($11 | 0) > -1 ? 1 : 0; - $15 = $7 - $5 | 0; - $10 = $8 - (($7 >>> 0 < $5 >>> 0) + $6 | 0) | 0; - if (($14 | 0) > ($18 | 0)) { - while (1) { - label$11 : { - if ($16 & 1) { - if (!($15 | $17 | ($10 | $11))) { - __multf3($9 + 32 | 0, $1, $2, $3, $4, 0, 0, 0, 0); - $3 = HEAP32[$9 + 40 >> 2]; - $4 = HEAP32[$9 + 44 >> 2]; - $7 = HEAP32[$9 + 32 >> 2]; - $8 = HEAP32[$9 + 36 >> 2]; - break label$1; - } - $7 = $17; - $16 = $11 << 1 | $7 >>> 31; - $17 = $7 << 1; - $11 = $16; - $16 = 0; - $7 = $10 >>> 31 | 0; - break label$11; - } - $11 = 0; - $10 = $8; - $17 = $8 >>> 31 | 0; - $15 = $7; - $7 = $13; - $16 = $12 << 1 | $7 >>> 31; - $7 = $7 << 1; - } - $13 = $7 | $17; - $8 = $13; - $7 = $21; - $17 = $8 - $7 | 0; - $12 = $11 | $16; - $11 = $12 - (($8 >>> 0 < $7 >>> 0) + $20 | 0) | 0; - $7 = $15; - $16 = $10 << 1 | $7 >>> 31; - $7 = $7 << 1; - $8 = $16; - $10 = ($6 | 0) == ($8 | 0) & $7 >>> 0 < $5 >>> 0 | $8 >>> 0 < $6 >>> 0; - $11 = $11 - ($17 >>> 0 < $10 >>> 0) | 0; - $17 = $17 - $10 | 0; - $16 = ($11 | 0) > -1 ? 1 : 0; - $15 = $7 - $5 | 0; - $10 = $8 - (($7 >>> 0 < $5 >>> 0) + $6 | 0) | 0; - $14 = $14 + -1 | 0; - if (($14 | 0) > ($18 | 0)) { - continue - } - break; - }; - $14 = $18; - } - label$14 : { - if (!$16) { - break label$14 - } - $7 = $15; - $13 = $17; - $8 = $10; - $12 = $11; - if ($7 | $13 | ($8 | $12)) { - break label$14 - } - __multf3($9 + 48 | 0, $1, $2, $3, $4, 0, 0, 0, 0); - $3 = HEAP32[$9 + 56 >> 2]; - $4 = HEAP32[$9 + 60 >> 2]; - $7 = HEAP32[$9 + 48 >> 2]; - $8 = HEAP32[$9 + 52 >> 2]; - break label$1; - } - if (($12 | 0) == 65535 | $12 >>> 0 < 65535) { - while (1) { - $3 = $8 >>> 31 | 0; - $1 = 0; - $14 = $14 + -1 | 0; - $11 = $8 << 1 | $7 >>> 31; - $7 = $7 << 1; - $8 = $11; - $2 = $13; - $16 = $12 << 1 | $2 >>> 31; - $13 = $2 << 1 | $3; - $1 = $1 | $16; - $12 = $1; - if (($1 | 0) == 65536 & $13 >>> 0 < 0 | $1 >>> 0 < 65536) { - continue - } - break; - } - } - $1 = $19 & 32768; - if (($14 | 0) <= 0) { - __multf3($9 - -64 | 0, $7, $8, $13, $12 & 65535 | ($1 | $14 + 120) << 16, 0, 0, 0, 1065811968); - $3 = HEAP32[$9 + 72 >> 2]; - $4 = HEAP32[$9 + 76 >> 2]; - $7 = HEAP32[$9 + 64 >> 2]; - $8 = HEAP32[$9 + 68 >> 2]; - break label$1; - } - $3 = $13; - $4 = $12 & 65535 | ($1 | $14) << 16; - } - HEAP32[$0 >> 2] = $7; - HEAP32[$0 + 4 >> 2] = $8; - HEAP32[$0 + 8 >> 2] = $3; - HEAP32[$0 + 12 >> 2] = $4; - global$0 = $9 + 128 | 0; - } - - function __floatscan($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0; - $5 = global$0 - 48 | 0; - global$0 = $5; - $4 = $1 + 4 | 0; - $7 = HEAP32[2644]; - $10 = HEAP32[2641]; - while (1) { - $2 = HEAP32[$1 + 4 >> 2]; - label$4 : { - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$4; - } - $2 = __shgetc($1); - } - if (($2 | 0) == 32 | $2 + -9 >>> 0 < 5) { - continue - } - break; - }; - $6 = 1; - label$6 : { - label$7 : { - switch ($2 + -43 | 0) { - case 0: - case 2: - break label$7; - default: - break label$6; - }; - } - $6 = ($2 | 0) == 45 ? -1 : 1; - $2 = HEAP32[$1 + 4 >> 2]; - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$6; - } - $2 = __shgetc($1); - } - label$1 : { - label$9 : { - label$10 : { - while (1) { - if (HEAP8[$3 + 10484 | 0] == ($2 | 32)) { - label$13 : { - if ($3 >>> 0 > 6) { - break label$13 - } - $2 = HEAP32[$1 + 4 >> 2]; - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$13; - } - $2 = __shgetc($1); - } - $3 = $3 + 1 | 0; - if (($3 | 0) != 8) { - continue - } - break label$10; - } - break; - }; - if (($3 | 0) != 3) { - if (($3 | 0) == 8) { - break label$10 - } - if ($3 >>> 0 < 4) { - break label$9 - } - if (($3 | 0) == 8) { - break label$10 - } - } - $1 = HEAP32[$1 + 104 >> 2]; - if ($1) { - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 - } - if ($3 >>> 0 < 4) { - break label$10 - } - while (1) { - if ($1) { - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 - } - $3 = $3 + -1 | 0; - if ($3 >>> 0 > 3) { - continue - } - break; - }; - } - __extendsftf2($5, Math_fround(Math_fround($6 | 0) * Math_fround(infinity))); - $6 = HEAP32[$5 + 8 >> 2]; - $2 = HEAP32[$5 + 12 >> 2]; - $8 = HEAP32[$5 >> 2]; - $9 = HEAP32[$5 + 4 >> 2]; - break label$1; - } - label$19 : { - label$20 : { - label$21 : { - if ($3) { - break label$21 - } - $3 = 0; - while (1) { - if (HEAP8[$3 + 10493 | 0] != ($2 | 32)) { - break label$21 - } - label$23 : { - if ($3 >>> 0 > 1) { - break label$23 - } - $2 = HEAP32[$1 + 4 >> 2]; - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$23; - } - $2 = __shgetc($1); - } - $3 = $3 + 1 | 0; - if (($3 | 0) != 3) { - continue - } - break; - }; - break label$20; - } - label$25 : { - switch ($3 | 0) { - case 0: - label$27 : { - if (($2 | 0) != 48) { - break label$27 - } - $3 = HEAP32[$1 + 4 >> 2]; - label$28 : { - if ($3 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $3 + 1; - $3 = HEAPU8[$3 | 0]; - break label$28; - } - $3 = __shgetc($1); - } - if (($3 & -33) == 88) { - hexfloat($5 + 16 | 0, $1, $10, $7, $6); - $6 = HEAP32[$5 + 24 >> 2]; - $2 = HEAP32[$5 + 28 >> 2]; - $8 = HEAP32[$5 + 16 >> 2]; - $9 = HEAP32[$5 + 20 >> 2]; - break label$1; - } - if (!HEAP32[$1 + 104 >> 2]) { - break label$27 - } - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1; - } - decfloat($5 + 32 | 0, $1, $2, $10, $7, $6); - $6 = HEAP32[$5 + 40 >> 2]; - $2 = HEAP32[$5 + 44 >> 2]; - $8 = HEAP32[$5 + 32 >> 2]; - $9 = HEAP32[$5 + 36 >> 2]; - break label$1; - case 3: - break label$20; - default: - break label$25; - }; - } - if (HEAP32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 - } - break label$19; - } - label$32 : { - $3 = HEAP32[$1 + 4 >> 2]; - label$33 : { - if ($3 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $3 + 1; - $2 = HEAPU8[$3 | 0]; - break label$33; - } - $2 = __shgetc($1); - } - if (($2 | 0) == 40) { - $3 = 1; - break label$32; - } - $6 = 0; - $2 = 2147450880; - if (!HEAP32[$1 + 104 >> 2]) { - break label$1 - } - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1; - break label$1; - } - while (1) { - $2 = HEAP32[$1 + 4 >> 2]; - label$37 : { - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$4 >> 2] = $2 + 1; - $7 = HEAPU8[$2 | 0]; - break label$37; - } - $7 = __shgetc($1); - } - if (!($7 + -97 >>> 0 >= 26 ? !($7 + -48 >>> 0 < 10 | $7 + -65 >>> 0 < 26 | ($7 | 0) == 95) : 0)) { - $3 = $3 + 1 | 0; - continue; - } - break; - }; - $6 = 0; - $2 = 2147450880; - if (($7 | 0) == 41) { - break label$1 - } - $1 = HEAP32[$1 + 104 >> 2]; - if ($1) { - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 - } - if (!$3) { - break label$1 - } - while (1) { - $3 = $3 + -1 | 0; - if ($1) { - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + -1 - } - if ($3) { - continue - } - break; - }; - break label$1; - } - HEAP32[2896] = 28; - __shlim($1); - $6 = 0; - $2 = 0; - } - HEAP32[$0 >> 2] = $8; - HEAP32[$0 + 4 >> 2] = $9; - HEAP32[$0 + 8 >> 2] = $6; - HEAP32[$0 + 12 >> 2] = $2; - global$0 = $5 + 48 | 0; - } - - function hexfloat($0, $1, $2, $3, $4) { - var $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0; - $5 = global$0 - 432 | 0; - global$0 = $5; - $6 = HEAP32[$1 + 4 >> 2]; - label$1 : { - if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $6 + 1; - $7 = HEAPU8[$6 | 0]; - break label$1; - } - $7 = __shgetc($1); - } - label$3 : { - label$4 : { - while (1) { - if (($7 | 0) != 48) { - label$6 : { - if (($7 | 0) != 46) { - break label$3 - } - $6 = HEAP32[$1 + 4 >> 2]; - if ($6 >>> 0 >= HEAPU32[$1 + 104 >> 2]) { - break label$6 - } - HEAP32[$1 + 4 >> 2] = $6 + 1; - $7 = HEAPU8[$6 | 0]; - break label$4; - } - } else { - $6 = HEAP32[$1 + 4 >> 2]; - if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $6 + 1; - $7 = HEAPU8[$6 | 0]; - } else { - $7 = __shgetc($1) - } - $21 = 1; - continue; - } - break; - }; - $7 = __shgetc($1); - } - $20 = 1; - if (($7 | 0) != 48) { - break label$3 - } - while (1) { - $6 = HEAP32[$1 + 4 >> 2]; - label$10 : { - if ($6 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $6 + 1; - $7 = HEAPU8[$6 | 0]; - break label$10; - } - $7 = __shgetc($1); - } - $13 = $13 + -1 | 0; - $17 = $17 + -1 | 0; - if (($17 | 0) != -1) { - $13 = $13 + 1 | 0 - } - if (($7 | 0) == 48) { - continue - } - break; - }; - $21 = 1; - } - $12 = 1073676288; - $6 = 0; - while (1) { - label$13 : { - $22 = $7 | 32; - label$14 : { - label$15 : { - $23 = $7 + -48 | 0; - if ($23 >>> 0 < 10) { - break label$15 - } - if ($22 + -97 >>> 0 > 5 ? ($7 | 0) != 46 : 0) { - break label$13 - } - if (($7 | 0) != 46) { - break label$15 - } - if ($20) { - break label$13 - } - $20 = 1; - $17 = $9; - $13 = $6; - break label$14; - } - $7 = ($7 | 0) > 57 ? $22 + -87 | 0 : $23; - label$16 : { - if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 7 ? 0 : 1) : 0) { - $14 = $7 + ($14 << 4) | 0; - break label$16; - } - if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 28 ? 0 : 1) : 0) { - __floatsitf($5 + 48 | 0, $7); - __multf3($5 + 32 | 0, $18, $19, $8, $12, 0, 0, 0, 1073414144); - $18 = HEAP32[$5 + 32 >> 2]; - $19 = HEAP32[$5 + 36 >> 2]; - $8 = HEAP32[$5 + 40 >> 2]; - $12 = HEAP32[$5 + 44 >> 2]; - __multf3($5 + 16 | 0, $18, $19, $8, $12, HEAP32[$5 + 48 >> 2], HEAP32[$5 + 52 >> 2], HEAP32[$5 + 56 >> 2], HEAP32[$5 + 60 >> 2]); - __addtf3($5, $10, $11, $15, $16, HEAP32[$5 + 16 >> 2], HEAP32[$5 + 20 >> 2], HEAP32[$5 + 24 >> 2], HEAP32[$5 + 28 >> 2]); - $15 = HEAP32[$5 + 8 >> 2]; - $16 = HEAP32[$5 + 12 >> 2]; - $10 = HEAP32[$5 >> 2]; - $11 = HEAP32[$5 + 4 >> 2]; - break label$16; - } - if (!$7 | $24) { - break label$16 - } - __multf3($5 + 80 | 0, $18, $19, $8, $12, 0, 0, 0, 1073610752); - __addtf3($5 - -64 | 0, $10, $11, $15, $16, HEAP32[$5 + 80 >> 2], HEAP32[$5 + 84 >> 2], HEAP32[$5 + 88 >> 2], HEAP32[$5 + 92 >> 2]); - $15 = HEAP32[$5 + 72 >> 2]; - $16 = HEAP32[$5 + 76 >> 2]; - $24 = 1; - $10 = HEAP32[$5 + 64 >> 2]; - $11 = HEAP32[$5 + 68 >> 2]; - } - $9 = $9 + 1 | 0; - if ($9 >>> 0 < 1) { - $6 = $6 + 1 | 0 - } - $21 = 1; - } - $7 = HEAP32[$1 + 4 >> 2]; - if ($7 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $7 + 1; - $7 = HEAPU8[$7 | 0]; - } else { - $7 = __shgetc($1) - } - continue; - } - break; - }; - label$20 : { - label$21 : { - if (!$21) { - if (!HEAP32[$1 + 104 >> 2]) { - break label$21 - } - $2 = HEAP32[$1 + 4 >> 2]; - HEAP32[$1 + 4 >> 2] = $2 + -1; - HEAP32[$1 + 4 >> 2] = $2 + -2; - if (!$20) { - break label$21 - } - HEAP32[$1 + 4 >> 2] = $2 + -3; - break label$21; - } - if (($6 | 0) < 0 ? 1 : ($6 | 0) <= 0 ? ($9 >>> 0 > 7 ? 0 : 1) : 0) { - $8 = $9; - $12 = $6; - while (1) { - $14 = $14 << 4; - $8 = $8 + 1 | 0; - if ($8 >>> 0 < 1) { - $12 = $12 + 1 | 0 - } - if (($8 | 0) != 8 | $12) { - continue - } - break; - }; - } - label$27 : { - if (($7 & -33) == 80) { - $8 = scanexp($1); - $7 = i64toi32_i32$HIGH_BITS; - $12 = $7; - if ($8 | ($7 | 0) != -2147483648) { - break label$27 - } - $8 = 0; - $12 = 0; - if (!HEAP32[$1 + 104 >> 2]) { - break label$27 - } - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; - break label$27; - } - $8 = 0; - $12 = 0; - if (!HEAP32[$1 + 104 >> 2]) { - break label$27 - } - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; - } - if (!$14) { - __extenddftf2($5 + 112 | 0, +($4 | 0) * 0.0); - $10 = HEAP32[$5 + 112 >> 2]; - $11 = HEAP32[$5 + 116 >> 2]; - $2 = HEAP32[$5 + 120 >> 2]; - $1 = HEAP32[$5 + 124 >> 2]; - break label$20; - } - $1 = $20 ? $17 : $9; - $6 = ($20 ? $13 : $6) << 2 | $1 >>> 30; - $1 = $8 + ($1 << 2) | 0; - $13 = $1 + -32 | 0; - $9 = $13; - $6 = $6 + $12 | 0; - $1 = ($1 >>> 0 < $8 >>> 0 ? $6 + 1 | 0 : $6) + -1 | 0; - $6 = $9 >>> 0 < 4294967264 ? $1 + 1 | 0 : $1; - if (($6 | 0) > 0 ? 1 : ($6 | 0) >= 0 ? ($9 >>> 0 <= 0 - $3 >>> 0 ? 0 : 1) : 0) { - HEAP32[2896] = 68; - __floatsitf($5 + 160 | 0, $4); - __multf3($5 + 144 | 0, HEAP32[$5 + 160 >> 2], HEAP32[$5 + 164 >> 2], HEAP32[$5 + 168 >> 2], HEAP32[$5 + 172 >> 2], -1, -1, -1, 2147418111); - __multf3($5 + 128 | 0, HEAP32[$5 + 144 >> 2], HEAP32[$5 + 148 >> 2], HEAP32[$5 + 152 >> 2], HEAP32[$5 + 156 >> 2], -1, -1, -1, 2147418111); - $10 = HEAP32[$5 + 128 >> 2]; - $11 = HEAP32[$5 + 132 >> 2]; - $2 = HEAP32[$5 + 136 >> 2]; - $1 = HEAP32[$5 + 140 >> 2]; - break label$20; - } - $1 = $3 + -226 | 0; - $7 = $9 >>> 0 < $1 >>> 0 ? 0 : 1; - $1 = $1 >> 31; - if (($6 | 0) > ($1 | 0) ? 1 : ($6 | 0) >= ($1 | 0) ? $7 : 0) { - if (($14 | 0) > -1) { - while (1) { - __addtf3($5 + 416 | 0, $10, $11, $15, $16, 0, 0, 0, -1073807360); - $1 = __getf2($10, $11, $15, $16, 1073610752); - $8 = ($1 | 0) < 0; - __addtf3($5 + 400 | 0, $10, $11, $15, $16, $8 ? $10 : HEAP32[$5 + 416 >> 2], $8 ? $11 : HEAP32[$5 + 420 >> 2], $8 ? $15 : HEAP32[$5 + 424 >> 2], $8 ? $16 : HEAP32[$5 + 428 >> 2]); - $6 = $6 + -1 | 0; - $9 = $9 + -1 | 0; - if (($9 | 0) != -1) { - $6 = $6 + 1 | 0 - } - $15 = HEAP32[$5 + 408 >> 2]; - $16 = HEAP32[$5 + 412 >> 2]; - $10 = HEAP32[$5 + 400 >> 2]; - $11 = HEAP32[$5 + 404 >> 2]; - $14 = $14 << 1 | ($1 | 0) > -1; - if (($14 | 0) > -1) { - continue - } - break; - } - } - $1 = ($9 - $3 | 0) + 32 | 0; - $8 = $1; - $7 = $2; - $12 = $1 >>> 0 >= $2 >>> 0 ? 0 : 1; - $2 = $6 - (($3 >> 31) + ($9 >>> 0 < $3 >>> 0) | 0) | 0; - $1 = $1 >>> 0 < 32 ? $2 + 1 | 0 : $2; - $1 = (($1 | 0) < 0 ? 1 : ($1 | 0) <= 0 ? $12 : 0) ? (($8 | 0) > 0 ? $8 : 0) : $7; - label$35 : { - if (($1 | 0) >= 113) { - __floatsitf($5 + 384 | 0, $4); - $17 = HEAP32[$5 + 392 >> 2]; - $13 = HEAP32[$5 + 396 >> 2]; - $18 = HEAP32[$5 + 384 >> 2]; - $19 = HEAP32[$5 + 388 >> 2]; - $6 = 0; - $4 = 0; - $3 = 0; - $2 = 0; - break label$35; - } - __extenddftf2($5 + 352 | 0, scalbn(1.0, 144 - $1 | 0)); - __floatsitf($5 + 336 | 0, $4); - $18 = HEAP32[$5 + 336 >> 2]; - $19 = HEAP32[$5 + 340 >> 2]; - $17 = HEAP32[$5 + 344 >> 2]; - $13 = HEAP32[$5 + 348 >> 2]; - copysignl($5 + 368 | 0, HEAP32[$5 + 352 >> 2], HEAP32[$5 + 356 >> 2], HEAP32[$5 + 360 >> 2], HEAP32[$5 + 364 >> 2], $18, $19, $17, $13); - $6 = HEAP32[$5 + 376 >> 2]; - $4 = HEAP32[$5 + 380 >> 2]; - $3 = HEAP32[$5 + 372 >> 2]; - $2 = HEAP32[$5 + 368 >> 2]; - } - $1 = !($14 & 1) & ((__letf2($10, $11, $15, $16, 0, 0, 0, 0) | 0) != 0 & ($1 | 0) < 32); - __floatunsitf($5 + 320 | 0, $1 + $14 | 0); - __multf3($5 + 304 | 0, $18, $19, $17, $13, HEAP32[$5 + 320 >> 2], HEAP32[$5 + 324 >> 2], HEAP32[$5 + 328 >> 2], HEAP32[$5 + 332 >> 2]); - __addtf3($5 + 272 | 0, HEAP32[$5 + 304 >> 2], HEAP32[$5 + 308 >> 2], HEAP32[$5 + 312 >> 2], HEAP32[$5 + 316 >> 2], $2, $3, $6, $4); - __multf3($5 + 288 | 0, $1 ? 0 : $10, $1 ? 0 : $11, $1 ? 0 : $15, $1 ? 0 : $16, $18, $19, $17, $13); - __addtf3($5 + 256 | 0, HEAP32[$5 + 288 >> 2], HEAP32[$5 + 292 >> 2], HEAP32[$5 + 296 >> 2], HEAP32[$5 + 300 >> 2], HEAP32[$5 + 272 >> 2], HEAP32[$5 + 276 >> 2], HEAP32[$5 + 280 >> 2], HEAP32[$5 + 284 >> 2]); - __subtf3($5 + 240 | 0, HEAP32[$5 + 256 >> 2], HEAP32[$5 + 260 >> 2], HEAP32[$5 + 264 >> 2], HEAP32[$5 + 268 >> 2], $2, $3, $6, $4); - $1 = HEAP32[$5 + 240 >> 2]; - $2 = HEAP32[$5 + 244 >> 2]; - $3 = HEAP32[$5 + 248 >> 2]; - $4 = HEAP32[$5 + 252 >> 2]; - if (!__letf2($1, $2, $3, $4, 0, 0, 0, 0)) { - HEAP32[2896] = 68 - } - scalbnl($5 + 224 | 0, $1, $2, $3, $4, $9); - $10 = HEAP32[$5 + 224 >> 2]; - $11 = HEAP32[$5 + 228 >> 2]; - $2 = HEAP32[$5 + 232 >> 2]; - $1 = HEAP32[$5 + 236 >> 2]; - break label$20; - } - HEAP32[2896] = 68; - __floatsitf($5 + 208 | 0, $4); - __multf3($5 + 192 | 0, HEAP32[$5 + 208 >> 2], HEAP32[$5 + 212 >> 2], HEAP32[$5 + 216 >> 2], HEAP32[$5 + 220 >> 2], 0, 0, 0, 65536); - __multf3($5 + 176 | 0, HEAP32[$5 + 192 >> 2], HEAP32[$5 + 196 >> 2], HEAP32[$5 + 200 >> 2], HEAP32[$5 + 204 >> 2], 0, 0, 0, 65536); - $10 = HEAP32[$5 + 176 >> 2]; - $11 = HEAP32[$5 + 180 >> 2]; - $2 = HEAP32[$5 + 184 >> 2]; - $1 = HEAP32[$5 + 188 >> 2]; - break label$20; - } - __extenddftf2($5 + 96 | 0, +($4 | 0) * 0.0); - $10 = HEAP32[$5 + 96 >> 2]; - $11 = HEAP32[$5 + 100 >> 2]; - $2 = HEAP32[$5 + 104 >> 2]; - $1 = HEAP32[$5 + 108 >> 2]; - } - HEAP32[$0 >> 2] = $10; - HEAP32[$0 + 4 >> 2] = $11; - HEAP32[$0 + 8 >> 2] = $2; - HEAP32[$0 + 12 >> 2] = $1; - global$0 = $5 + 432 | 0; - } - - function decfloat($0, $1, $2, $3, $4, $5) { - var $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0.0, $25 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0, wasm2js_i32$2 = 0; - $6 = global$0 - 8976 | 0; - global$0 = $6; - $22 = $3 + $4 | 0; - $25 = 0 - $22 | 0; - label$1 : { - label$2 : { - while (1) { - if (($2 | 0) != 48) { - label$4 : { - if (($2 | 0) != 46) { - break label$1 - } - $2 = HEAP32[$1 + 4 >> 2]; - if ($2 >>> 0 >= HEAPU32[$1 + 104 >> 2]) { - break label$4 - } - HEAP32[$1 + 4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$2; - } - } else { - $2 = HEAP32[$1 + 4 >> 2]; - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - $9 = 1; - HEAP32[$1 + 4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - } else { - $9 = 1; - $2 = __shgetc($1); - } - continue; - } - break; - }; - $2 = __shgetc($1); - } - $14 = 1; - if (($2 | 0) != 48) { - break label$1 - } - while (1) { - $2 = HEAP32[$1 + 4 >> 2]; - label$8 : { - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$8; - } - $2 = __shgetc($1); - } - $7 = $7 + -1 | 0; - $8 = $8 + -1 | 0; - if (($8 | 0) != -1) { - $7 = $7 + 1 | 0 - } - if (($2 | 0) == 48) { - continue - } - break; - }; - $9 = 1; - } - HEAP32[$6 + 784 >> 2] = 0; - label$10 : { - label$11 : { - $12 = ($2 | 0) == 46; - $13 = $2 + -48 | 0; - label$13 : { - label$14 : { - label$15 : { - if ($12 | $13 >>> 0 <= 9) { - while (1) { - label$19 : { - if ($12 & 1) { - if (!$14) { - $8 = $10; - $7 = $11; - $14 = 1; - break label$19; - } - $9 = !$9; - break label$15; - } - $10 = $10 + 1 | 0; - if ($10 >>> 0 < 1) { - $11 = $11 + 1 | 0 - } - if (($15 | 0) <= 2044) { - $20 = ($2 | 0) == 48 ? $20 : $10; - $9 = ($6 + 784 | 0) + ($15 << 2) | 0; - HEAP32[$9 >> 2] = $17 ? (Math_imul(HEAP32[$9 >> 2], 10) + $2 | 0) + -48 | 0 : $13; - $9 = 1; - $13 = $17 + 1 | 0; - $2 = ($13 | 0) == 9; - $17 = $2 ? 0 : $13; - $15 = $2 + $15 | 0; - break label$19; - } - if (($2 | 0) == 48) { - break label$19 - } - HEAP32[$6 + 8960 >> 2] = HEAP32[$6 + 8960 >> 2] | 1; - $20 = 18396; - } - $2 = HEAP32[$1 + 4 >> 2]; - label$25 : { - if ($2 >>> 0 < HEAPU32[$1 + 104 >> 2]) { - HEAP32[$1 + 4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$25; - } - $2 = __shgetc($1); - } - $12 = ($2 | 0) == 46; - $13 = $2 + -48 | 0; - if ($12 | $13 >>> 0 < 10) { - continue - } - break; - } - } - $8 = $14 ? $8 : $10; - $7 = $14 ? $7 : $11; - if (!(!$9 | ($2 & -33) != 69)) { - $12 = scanexp($1); - $2 = i64toi32_i32$HIGH_BITS; - $16 = $2; - label$28 : { - if ($12 | ($2 | 0) != -2147483648) { - break label$28 - } - $12 = 0; - $16 = 0; - if (!HEAP32[$1 + 104 >> 2]) { - break label$28 - } - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; - } - if (!$9) { - break label$13 - } - $7 = $7 + $16 | 0; - $8 = $8 + $12 | 0; - if ($8 >>> 0 < $12 >>> 0) { - $7 = $7 + 1 | 0 - } - break label$11; - } - $9 = !$9; - if (($2 | 0) < 0) { - break label$14 - } - } - if (!HEAP32[$1 + 104 >> 2]) { - break label$14 - } - HEAP32[$1 + 4 >> 2] = HEAP32[$1 + 4 >> 2] + -1; - } - if (!$9) { - break label$11 - } - } - HEAP32[2896] = 28; - $10 = 0; - $11 = 0; - __shlim($1); - $2 = 0; - $1 = 0; - break label$10; - } - $1 = HEAP32[$6 + 784 >> 2]; - if (!$1) { - __extenddftf2($6, +($5 | 0) * 0.0); - $10 = HEAP32[$6 >> 2]; - $11 = HEAP32[$6 + 4 >> 2]; - $2 = HEAP32[$6 + 8 >> 2]; - $1 = HEAP32[$6 + 12 >> 2]; - break label$10; - } - if (!(($8 | 0) != ($10 | 0) | ($7 | 0) != ($11 | 0) | (($11 | 0) > 0 ? 1 : ($11 | 0) >= 0 ? ($10 >>> 0 <= 9 ? 0 : 1) : 0) | ($1 >>> $3 | 0 ? ($3 | 0) <= 30 : 0))) { - __floatsitf($6 + 48 | 0, $5); - __floatunsitf($6 + 32 | 0, $1); - __multf3($6 + 16 | 0, HEAP32[$6 + 48 >> 2], HEAP32[$6 + 52 >> 2], HEAP32[$6 + 56 >> 2], HEAP32[$6 + 60 >> 2], HEAP32[$6 + 32 >> 2], HEAP32[$6 + 36 >> 2], HEAP32[$6 + 40 >> 2], HEAP32[$6 + 44 >> 2]); - $10 = HEAP32[$6 + 16 >> 2]; - $11 = HEAP32[$6 + 20 >> 2]; - $2 = HEAP32[$6 + 24 >> 2]; - $1 = HEAP32[$6 + 28 >> 2]; - break label$10; - } - if (($7 | 0) > 0 ? 1 : ($7 | 0) >= 0 ? ($8 >>> 0 <= ($4 | 0) / -2 >>> 0 ? 0 : 1) : 0) { - HEAP32[2896] = 68; - __floatsitf($6 + 96 | 0, $5); - __multf3($6 + 80 | 0, HEAP32[$6 + 96 >> 2], HEAP32[$6 + 100 >> 2], HEAP32[$6 + 104 >> 2], HEAP32[$6 + 108 >> 2], -1, -1, -1, 2147418111); - __multf3($6 - -64 | 0, HEAP32[$6 + 80 >> 2], HEAP32[$6 + 84 >> 2], HEAP32[$6 + 88 >> 2], HEAP32[$6 + 92 >> 2], -1, -1, -1, 2147418111); - $10 = HEAP32[$6 + 64 >> 2]; - $11 = HEAP32[$6 + 68 >> 2]; - $2 = HEAP32[$6 + 72 >> 2]; - $1 = HEAP32[$6 + 76 >> 2]; - break label$10; - } - $1 = $4 + -226 | 0; - $2 = $8 >>> 0 >= $1 >>> 0 ? 0 : 1; - $1 = $1 >> 31; - if (($7 | 0) < ($1 | 0) ? 1 : ($7 | 0) <= ($1 | 0) ? $2 : 0) { - HEAP32[2896] = 68; - __floatsitf($6 + 144 | 0, $5); - __multf3($6 + 128 | 0, HEAP32[$6 + 144 >> 2], HEAP32[$6 + 148 >> 2], HEAP32[$6 + 152 >> 2], HEAP32[$6 + 156 >> 2], 0, 0, 0, 65536); - __multf3($6 + 112 | 0, HEAP32[$6 + 128 >> 2], HEAP32[$6 + 132 >> 2], HEAP32[$6 + 136 >> 2], HEAP32[$6 + 140 >> 2], 0, 0, 0, 65536); - $10 = HEAP32[$6 + 112 >> 2]; - $11 = HEAP32[$6 + 116 >> 2]; - $2 = HEAP32[$6 + 120 >> 2]; - $1 = HEAP32[$6 + 124 >> 2]; - break label$10; - } - if ($17) { - if (($17 | 0) <= 8) { - $2 = ($6 + 784 | 0) + ($15 << 2) | 0; - $1 = HEAP32[$2 >> 2]; - while (1) { - $1 = Math_imul($1, 10); - $17 = $17 + 1 | 0; - if (($17 | 0) != 9) { - continue - } - break; - }; - HEAP32[$2 >> 2] = $1; - } - $15 = $15 + 1 | 0; - } - label$36 : { - $14 = $8; - if (($20 | 0) > ($8 | 0) | ($20 | 0) >= 9 | ($8 | 0) > 17) { - break label$36 - } - if (($14 | 0) == 9) { - __floatsitf($6 + 192 | 0, $5); - __floatunsitf($6 + 176 | 0, HEAP32[$6 + 784 >> 2]); - __multf3($6 + 160 | 0, HEAP32[$6 + 192 >> 2], HEAP32[$6 + 196 >> 2], HEAP32[$6 + 200 >> 2], HEAP32[$6 + 204 >> 2], HEAP32[$6 + 176 >> 2], HEAP32[$6 + 180 >> 2], HEAP32[$6 + 184 >> 2], HEAP32[$6 + 188 >> 2]); - $10 = HEAP32[$6 + 160 >> 2]; - $11 = HEAP32[$6 + 164 >> 2]; - $2 = HEAP32[$6 + 168 >> 2]; - $1 = HEAP32[$6 + 172 >> 2]; - break label$10; - } - if (($14 | 0) <= 8) { - __floatsitf($6 + 272 | 0, $5); - __floatunsitf($6 + 256 | 0, HEAP32[$6 + 784 >> 2]); - __multf3($6 + 240 | 0, HEAP32[$6 + 272 >> 2], HEAP32[$6 + 276 >> 2], HEAP32[$6 + 280 >> 2], HEAP32[$6 + 284 >> 2], HEAP32[$6 + 256 >> 2], HEAP32[$6 + 260 >> 2], HEAP32[$6 + 264 >> 2], HEAP32[$6 + 268 >> 2]); - __floatsitf($6 + 224 | 0, HEAP32[(0 - $14 << 2) + 10560 >> 2]); - __divtf3($6 + 208 | 0, HEAP32[$6 + 240 >> 2], HEAP32[$6 + 244 >> 2], HEAP32[$6 + 248 >> 2], HEAP32[$6 + 252 >> 2], HEAP32[$6 + 224 >> 2], HEAP32[$6 + 228 >> 2], HEAP32[$6 + 232 >> 2], HEAP32[$6 + 236 >> 2]); - $10 = HEAP32[$6 + 208 >> 2]; - $11 = HEAP32[$6 + 212 >> 2]; - $2 = HEAP32[$6 + 216 >> 2]; - $1 = HEAP32[$6 + 220 >> 2]; - break label$10; - } - $1 = (Math_imul($14, -3) + $3 | 0) + 27 | 0; - $2 = HEAP32[$6 + 784 >> 2]; - if ($2 >>> $1 | 0 ? ($1 | 0) <= 30 : 0) { - break label$36 - } - __floatsitf($6 + 352 | 0, $5); - __floatunsitf($6 + 336 | 0, $2); - __multf3($6 + 320 | 0, HEAP32[$6 + 352 >> 2], HEAP32[$6 + 356 >> 2], HEAP32[$6 + 360 >> 2], HEAP32[$6 + 364 >> 2], HEAP32[$6 + 336 >> 2], HEAP32[$6 + 340 >> 2], HEAP32[$6 + 344 >> 2], HEAP32[$6 + 348 >> 2]); - __floatsitf($6 + 304 | 0, HEAP32[($14 << 2) + 10488 >> 2]); - __multf3($6 + 288 | 0, HEAP32[$6 + 320 >> 2], HEAP32[$6 + 324 >> 2], HEAP32[$6 + 328 >> 2], HEAP32[$6 + 332 >> 2], HEAP32[$6 + 304 >> 2], HEAP32[$6 + 308 >> 2], HEAP32[$6 + 312 >> 2], HEAP32[$6 + 316 >> 2]); - $10 = HEAP32[$6 + 288 >> 2]; - $11 = HEAP32[$6 + 292 >> 2]; - $2 = HEAP32[$6 + 296 >> 2]; - $1 = HEAP32[$6 + 300 >> 2]; - break label$10; - } - while (1) { - $2 = $15; - $15 = $2 + -1 | 0; - if (!HEAP32[($6 + 784 | 0) + ($15 << 2) >> 2]) { - continue - } - break; - }; - $17 = 0; - $1 = ($14 | 0) % 9 | 0; - label$40 : { - if (!$1) { - $9 = 0; - break label$40; - } - $13 = ($14 | 0) > -1 ? $1 : $1 + 9 | 0; - label$42 : { - if (!$2) { - $9 = 0; - $2 = 0; - break label$42; - } - $8 = HEAP32[(0 - $13 << 2) + 10560 >> 2]; - $10 = 1e9 / ($8 | 0) | 0; - $12 = 0; - $1 = 0; - $9 = 0; - while (1) { - $11 = ($6 + 784 | 0) + ($1 << 2) | 0; - $15 = HEAP32[$11 >> 2]; - $16 = ($15 >>> 0) / ($8 >>> 0) | 0; - $7 = $12 + $16 | 0; - HEAP32[$11 >> 2] = $7; - $7 = !$7 & ($1 | 0) == ($9 | 0); - $9 = $7 ? $9 + 1 & 2047 : $9; - $14 = $7 ? $14 + -9 | 0 : $14; - $12 = Math_imul($10, $15 - Math_imul($8, $16) | 0); - $1 = $1 + 1 | 0; - if (($2 | 0) != ($1 | 0)) { - continue - } - break; - }; - if (!$12) { - break label$42 - } - HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2] = $12; - $2 = $2 + 1 | 0; - } - $14 = ($14 - $13 | 0) + 9 | 0; - } - while (1) { - $11 = ($6 + 784 | 0) + ($9 << 2) | 0; - label$46 : { - while (1) { - if (($14 | 0) != 36 | HEAPU32[$11 >> 2] >= 10384593 ? ($14 | 0) >= 36 : 0) { - break label$46 - } - $15 = $2 + 2047 | 0; - $12 = 0; - $13 = $2; - while (1) { - $2 = $13; - $10 = $15 & 2047; - $13 = ($6 + 784 | 0) + ($10 << 2) | 0; - $1 = HEAP32[$13 >> 2]; - $7 = $1 >>> 3 | 0; - $1 = $1 << 29; - $8 = $1 + $12 | 0; - if ($8 >>> 0 < $1 >>> 0) { - $7 = $7 + 1 | 0 - } - $1 = 0; - if (!(!$7 & $8 >>> 0 < 1000000001 | $7 >>> 0 < 0)) { - $1 = __wasm_i64_udiv($8, $7, 1e9); - $8 = $8 - __wasm_i64_mul($1, i64toi32_i32$HIGH_BITS, 1e9, 0) | 0; - } - $12 = $1; - HEAP32[$13 >> 2] = $8; - $13 = ($10 | 0) != ($2 + -1 & 2047) ? $2 : ($9 | 0) == ($10 | 0) ? $2 : $8 ? $2 : $10; - $15 = $10 + -1 | 0; - if (($9 | 0) != ($10 | 0)) { - continue - } - break; - }; - $17 = $17 + -29 | 0; - if (!$12) { - continue - } - break; - }; - $9 = $9 + -1 & 2047; - if (($13 | 0) == ($9 | 0)) { - $1 = ($6 + 784 | 0) + (($13 + 2046 & 2047) << 2) | 0; - $2 = $13 + -1 & 2047; - HEAP32[$1 >> 2] = HEAP32[$1 >> 2] | HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2]; - } - $14 = $14 + 9 | 0; - HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2] = $12; - continue; - } - break; - }; - label$52 : { - label$53 : while (1) { - $8 = $2 + 1 & 2047; - $10 = ($6 + 784 | 0) + (($2 + -1 & 2047) << 2) | 0; - while (1) { - $7 = ($14 | 0) > 45 ? 9 : 1; - label$55 : { - while (1) { - $13 = $9; - $1 = 0; - label$57 : { - while (1) { - label$59 : { - $9 = $1 + $13 & 2047; - if (($9 | 0) == ($2 | 0)) { - break label$59 - } - $9 = HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2]; - $11 = HEAP32[($1 << 2) + 10512 >> 2]; - if ($9 >>> 0 < $11 >>> 0) { - break label$59 - } - if ($9 >>> 0 > $11 >>> 0) { - break label$57 - } - $1 = $1 + 1 | 0; - if (($1 | 0) != 4) { - continue - } - } - break; - }; - if (($14 | 0) != 36) { - break label$57 - } - $8 = 0; - $7 = 0; - $1 = 0; - $10 = 0; - $11 = 0; - while (1) { - $9 = $1 + $13 & 2047; - if (($9 | 0) == ($2 | 0)) { - $2 = $2 + 1 & 2047; - HEAP32[(($2 << 2) + $6 | 0) + 780 >> 2] = 0; - } - __multf3($6 + 768 | 0, $8, $7, $10, $11, 0, 0, 1342177280, 1075633366); - __floatunsitf($6 + 752 | 0, HEAP32[($6 + 784 | 0) + ($9 << 2) >> 2]); - __addtf3($6 + 736 | 0, HEAP32[$6 + 768 >> 2], HEAP32[$6 + 772 >> 2], HEAP32[$6 + 776 >> 2], HEAP32[$6 + 780 >> 2], HEAP32[$6 + 752 >> 2], HEAP32[$6 + 756 >> 2], HEAP32[$6 + 760 >> 2], HEAP32[$6 + 764 >> 2]); - $10 = HEAP32[$6 + 744 >> 2]; - $11 = HEAP32[$6 + 748 >> 2]; - $8 = HEAP32[$6 + 736 >> 2]; - $7 = HEAP32[$6 + 740 >> 2]; - $1 = $1 + 1 | 0; - if (($1 | 0) != 4) { - continue - } - break; - }; - __floatsitf($6 + 720 | 0, $5); - __multf3($6 + 704 | 0, $8, $7, $10, $11, HEAP32[$6 + 720 >> 2], HEAP32[$6 + 724 >> 2], HEAP32[$6 + 728 >> 2], HEAP32[$6 + 732 >> 2]); - $10 = HEAP32[$6 + 712 >> 2]; - $11 = HEAP32[$6 + 716 >> 2]; - $8 = 0; - $7 = 0; - $12 = HEAP32[$6 + 704 >> 2]; - $16 = HEAP32[$6 + 708 >> 2]; - $23 = $17 + 113 | 0; - $4 = $23 - $4 | 0; - $20 = ($4 | 0) < ($3 | 0); - $1 = $20 ? (($4 | 0) > 0 ? $4 : 0) : $3; - if (($1 | 0) <= 112) { - break label$55 - } - $14 = 0; - $15 = 0; - $9 = 0; - $3 = 0; - break label$52; - } - $17 = $7 + $17 | 0; - $9 = $2; - if (($2 | 0) == ($13 | 0)) { - continue - } - break; - }; - $11 = 1e9 >>> $7 | 0; - $12 = -1 << $7 ^ -1; - $1 = 0; - $9 = $13; - while (1) { - $15 = ($6 + 784 | 0) + ($13 << 2) | 0; - $16 = HEAP32[$15 >> 2]; - $1 = $1 + ($16 >>> $7 | 0) | 0; - HEAP32[$15 >> 2] = $1; - $1 = !$1 & ($9 | 0) == ($13 | 0); - $9 = $1 ? $9 + 1 & 2047 : $9; - $14 = $1 ? $14 + -9 | 0 : $14; - $1 = Math_imul($11, $12 & $16); - $13 = $13 + 1 & 2047; - if (($13 | 0) != ($2 | 0)) { - continue - } - break; - }; - if (!$1) { - continue - } - if (($8 | 0) != ($9 | 0)) { - HEAP32[($6 + 784 | 0) + ($2 << 2) >> 2] = $1; - $2 = $8; - continue label$53; - } - HEAP32[$10 >> 2] = HEAP32[$10 >> 2] | 1; - $9 = $8; - continue; - } - break; - }; - break; - }; - __extenddftf2($6 + 656 | 0, scalbn(1.0, 225 - $1 | 0)); - copysignl($6 + 688 | 0, HEAP32[$6 + 656 >> 2], HEAP32[$6 + 660 >> 2], HEAP32[$6 + 664 >> 2], HEAP32[$6 + 668 >> 2], $12, $16, $10, $11); - $9 = HEAP32[$6 + 696 >> 2]; - $3 = HEAP32[$6 + 700 >> 2]; - $14 = HEAP32[$6 + 688 >> 2]; - $15 = HEAP32[$6 + 692 >> 2]; - __extenddftf2($6 + 640 | 0, scalbn(1.0, 113 - $1 | 0)); - fmodl($6 + 672 | 0, $12, $16, $10, $11, HEAP32[$6 + 640 >> 2], HEAP32[$6 + 644 >> 2], HEAP32[$6 + 648 >> 2], HEAP32[$6 + 652 >> 2]); - $8 = HEAP32[$6 + 672 >> 2]; - $7 = HEAP32[$6 + 676 >> 2]; - $18 = HEAP32[$6 + 680 >> 2]; - $19 = HEAP32[$6 + 684 >> 2]; - __subtf3($6 + 624 | 0, $12, $16, $10, $11, $8, $7, $18, $19); - __addtf3($6 + 608 | 0, $14, $15, $9, $3, HEAP32[$6 + 624 >> 2], HEAP32[$6 + 628 >> 2], HEAP32[$6 + 632 >> 2], HEAP32[$6 + 636 >> 2]); - $10 = HEAP32[$6 + 616 >> 2]; - $11 = HEAP32[$6 + 620 >> 2]; - $12 = HEAP32[$6 + 608 >> 2]; - $16 = HEAP32[$6 + 612 >> 2]; - } - $21 = $13 + 4 & 2047; - label$64 : { - if (($21 | 0) == ($2 | 0)) { - break label$64 - } - $21 = HEAP32[($6 + 784 | 0) + ($21 << 2) >> 2]; - label$65 : { - if ($21 >>> 0 <= 499999999) { - if (($13 + 5 & 2047) == ($2 | 0) ? !$21 : 0) { - break label$65 - } - __extenddftf2($6 + 496 | 0, +($5 | 0) * .25); - __addtf3($6 + 480 | 0, $8, $7, $18, $19, HEAP32[$6 + 496 >> 2], HEAP32[$6 + 500 >> 2], HEAP32[$6 + 504 >> 2], HEAP32[$6 + 508 >> 2]); - $18 = HEAP32[$6 + 488 >> 2]; - $19 = HEAP32[$6 + 492 >> 2]; - $8 = HEAP32[$6 + 480 >> 2]; - $7 = HEAP32[$6 + 484 >> 2]; - break label$65; - } - if (($21 | 0) != 5e8) { - __extenddftf2($6 + 592 | 0, +($5 | 0) * .75); - __addtf3($6 + 576 | 0, $8, $7, $18, $19, HEAP32[$6 + 592 >> 2], HEAP32[$6 + 596 >> 2], HEAP32[$6 + 600 >> 2], HEAP32[$6 + 604 >> 2]); - $18 = HEAP32[$6 + 584 >> 2]; - $19 = HEAP32[$6 + 588 >> 2]; - $8 = HEAP32[$6 + 576 >> 2]; - $7 = HEAP32[$6 + 580 >> 2]; - break label$65; - } - $24 = +($5 | 0); - if (($13 + 5 & 2047) == ($2 | 0)) { - __extenddftf2($6 + 528 | 0, $24 * .5); - __addtf3($6 + 512 | 0, $8, $7, $18, $19, HEAP32[$6 + 528 >> 2], HEAP32[$6 + 532 >> 2], HEAP32[$6 + 536 >> 2], HEAP32[$6 + 540 >> 2]); - $18 = HEAP32[$6 + 520 >> 2]; - $19 = HEAP32[$6 + 524 >> 2]; - $8 = HEAP32[$6 + 512 >> 2]; - $7 = HEAP32[$6 + 516 >> 2]; - break label$65; - } - __extenddftf2($6 + 560 | 0, $24 * .75); - __addtf3($6 + 544 | 0, $8, $7, $18, $19, HEAP32[$6 + 560 >> 2], HEAP32[$6 + 564 >> 2], HEAP32[$6 + 568 >> 2], HEAP32[$6 + 572 >> 2]); - $18 = HEAP32[$6 + 552 >> 2]; - $19 = HEAP32[$6 + 556 >> 2]; - $8 = HEAP32[$6 + 544 >> 2]; - $7 = HEAP32[$6 + 548 >> 2]; - } - if (($1 | 0) > 111) { - break label$64 - } - fmodl($6 + 464 | 0, $8, $7, $18, $19, 0, 0, 0, 1073676288); - if (__letf2(HEAP32[$6 + 464 >> 2], HEAP32[$6 + 468 >> 2], HEAP32[$6 + 472 >> 2], HEAP32[$6 + 476 >> 2], 0, 0, 0, 0)) { - break label$64 - } - __addtf3($6 + 448 | 0, $8, $7, $18, $19, 0, 0, 0, 1073676288); - $18 = HEAP32[$6 + 456 >> 2]; - $19 = HEAP32[$6 + 460 >> 2]; - $8 = HEAP32[$6 + 448 >> 2]; - $7 = HEAP32[$6 + 452 >> 2]; - } - __addtf3($6 + 432 | 0, $12, $16, $10, $11, $8, $7, $18, $19); - __subtf3($6 + 416 | 0, HEAP32[$6 + 432 >> 2], HEAP32[$6 + 436 >> 2], HEAP32[$6 + 440 >> 2], HEAP32[$6 + 444 >> 2], $14, $15, $9, $3); - $10 = HEAP32[$6 + 424 >> 2]; - $11 = HEAP32[$6 + 428 >> 2]; - $12 = HEAP32[$6 + 416 >> 2]; - $16 = HEAP32[$6 + 420 >> 2]; - label$69 : { - if (($23 & 2147483647) <= (-2 - $22 | 0)) { - break label$69 - } - $2 = $6 + 400 | 0; - HEAP32[$2 + 8 >> 2] = $10; - HEAP32[$2 + 12 >> 2] = $11 & 2147483647; - HEAP32[$2 >> 2] = $12; - HEAP32[$2 + 4 >> 2] = $16; - __multf3($6 + 384 | 0, $12, $16, $10, $11, 0, 0, 0, 1073610752); - $3 = __getf2(HEAP32[$6 + 400 >> 2], HEAP32[$6 + 404 >> 2], HEAP32[$6 + 408 >> 2], HEAP32[$6 + 412 >> 2], 1081081856); - $2 = ($3 | 0) < 0; - $10 = $2 ? $10 : HEAP32[$6 + 392 >> 2]; - $11 = $2 ? $11 : HEAP32[$6 + 396 >> 2]; - $12 = $2 ? $12 : HEAP32[$6 + 384 >> 2]; - $16 = $2 ? $16 : HEAP32[$6 + 388 >> 2]; - $17 = (($3 | 0) > -1) + $17 | 0; - if (wasm2js_i32$0 = !($20 & ($2 | ($1 | 0) != ($4 | 0)) & (__letf2($8, $7, $18, $19, 0, 0, 0, 0) | 0) != 0), wasm2js_i32$1 = 0, wasm2js_i32$2 = ($17 + 110 | 0) <= ($25 | 0), wasm2js_i32$2 ? wasm2js_i32$0 : wasm2js_i32$1) { - break label$69 - } - HEAP32[2896] = 68; - } - scalbnl($6 + 368 | 0, $12, $16, $10, $11, $17); - $10 = HEAP32[$6 + 368 >> 2]; - $11 = HEAP32[$6 + 372 >> 2]; - $2 = HEAP32[$6 + 376 >> 2]; - $1 = HEAP32[$6 + 380 >> 2]; - } - HEAP32[$0 >> 2] = $10; - HEAP32[$0 + 4 >> 2] = $11; - HEAP32[$0 + 8 >> 2] = $2; - HEAP32[$0 + 12 >> 2] = $1; - global$0 = $6 + 8976 | 0; - } - - function scanexp($0) { - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0; - label$1 : { - label$2 : { - label$3 : { - $3 = HEAP32[$0 + 4 >> 2]; - label$4 : { - if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = $3 + 1; - $2 = HEAPU8[$3 | 0]; - break label$4; - } - $2 = __shgetc($0); - } - switch ($2 + -43 | 0) { - case 0: - case 2: - break label$2; - default: - break label$3; - }; - } - $1 = $2 + -48 | 0; - break label$1; - } - $5 = ($2 | 0) == 45; - $3 = HEAP32[$0 + 4 >> 2]; - label$6 : { - if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = $3 + 1; - $2 = HEAPU8[$3 | 0]; - break label$6; - } - $2 = __shgetc($0); - } - $1 = $2 + -48 | 0; - if (!($1 >>> 0 < 10 | !HEAP32[$0 + 104 >> 2])) { - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1 - } - } - label$9 : { - if ($1 >>> 0 < 10) { - $1 = 0; - while (1) { - $1 = Math_imul($1, 10) + $2 | 0; - $3 = HEAP32[$0 + 4 >> 2]; - label$12 : { - if ($3 >>> 0 < HEAPU32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = $3 + 1; - $2 = HEAPU8[$3 | 0]; - break label$12; - } - $2 = __shgetc($0); - } - $4 = $2 + -48 | 0; - $1 = $1 + -48 | 0; - if (($1 | 0) < 214748364 ? $4 >>> 0 <= 9 : 0) { - continue - } - break; - }; - $3 = $1; - $1 = $1 >> 31; - label$14 : { - if ($4 >>> 0 >= 10) { - break label$14 - } - while (1) { - $1 = __wasm_i64_mul($3, $1, 10, 0); - $3 = $1 + $2 | 0; - $2 = i64toi32_i32$HIGH_BITS; - $4 = $3 >>> 0 < $1 >>> 0 ? $2 + 1 | 0 : $2; - $1 = HEAP32[$0 + 4 >> 2]; - label$16 : { - if ($1 >>> 0 < HEAPU32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = $1 + 1; - $2 = HEAPU8[$1 | 0]; - break label$16; - } - $2 = __shgetc($0); - } - $1 = $4 + -1 | 0; - $3 = $3 + -48 | 0; - if ($3 >>> 0 < 4294967248) { - $1 = $1 + 1 | 0 - } - $4 = $2 + -48 | 0; - if ($4 >>> 0 > 9) { - break label$14 - } - if (($1 | 0) < 21474836 ? 1 : ($1 | 0) <= 21474836 ? ($3 >>> 0 >= 2061584302 ? 0 : 1) : 0) { - continue - } - break; - }; - } - if ($4 >>> 0 < 10) { - while (1) { - $2 = HEAP32[$0 + 4 >> 2]; - label$20 : { - if ($2 >>> 0 < HEAPU32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = $2 + 1; - $2 = HEAPU8[$2 | 0]; - break label$20; - } - $2 = __shgetc($0); - } - if ($2 + -48 >>> 0 < 10) { - continue - } - break; - } - } - if (HEAP32[$0 + 104 >> 2]) { - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1 - } - $0 = $3; - $3 = $5 ? 0 - $0 | 0 : $0; - $1 = $5 ? 0 - ($1 + (0 < $0 >>> 0) | 0) | 0 : $1; - break label$9; - } - $3 = 0; - $1 = -2147483648; - if (!HEAP32[$0 + 104 >> 2]) { - break label$9 - } - HEAP32[$0 + 4 >> 2] = HEAP32[$0 + 4 >> 2] + -1; - i64toi32_i32$HIGH_BITS = -2147483648; - return 0; - } - i64toi32_i32$HIGH_BITS = $1; - return $3; - } - - function strtox($0, $1) { - var $2 = 0, $3 = 0, $4 = 0; - $2 = global$0 - 160 | 0; - global$0 = $2; - memset($2 + 16 | 0, 144); - HEAP32[$2 + 92 >> 2] = -1; - HEAP32[$2 + 60 >> 2] = $1; - HEAP32[$2 + 24 >> 2] = -1; - HEAP32[$2 + 20 >> 2] = $1; - __shlim($2 + 16 | 0); - __floatscan($2, $2 + 16 | 0); - $1 = HEAP32[$2 + 8 >> 2]; - $3 = HEAP32[$2 + 12 >> 2]; - $4 = HEAP32[$2 + 4 >> 2]; - HEAP32[$0 >> 2] = HEAP32[$2 >> 2]; - HEAP32[$0 + 4 >> 2] = $4; - HEAP32[$0 + 8 >> 2] = $1; - HEAP32[$0 + 12 >> 2] = $3; - global$0 = $2 + 160 | 0; - } - - function strtod($0) { - var $1 = 0, $2 = 0.0; - $1 = global$0 - 16 | 0; - global$0 = $1; - strtox($1, $0); - $2 = __trunctfdf2(HEAP32[$1 >> 2], HEAP32[$1 + 4 >> 2], HEAP32[$1 + 8 >> 2], HEAP32[$1 + 12 >> 2]); - global$0 = $1 + 16 | 0; - return $2; - } - - function FLAC__stream_encoder_new() { - var $0 = 0, $1 = 0, $2 = 0, $3 = 0; - $1 = dlcalloc(1, 8); - if (!$1) { - return 0 - } - $0 = dlcalloc(1, 1032); - HEAP32[$1 >> 2] = $0; - label$2 : { - if (!$0) { - break label$2 - } - $3 = dlcalloc(1, 11856); - HEAP32[$1 + 4 >> 2] = $3; - if (!$3) { - dlfree($0); - break label$2; - } - $0 = dlcalloc(1, 20); - $3 = HEAP32[$1 + 4 >> 2]; - HEAP32[$3 + 6856 >> 2] = $0; - if (!$0) { - dlfree($3); - dlfree(HEAP32[$1 >> 2]); - break label$2; - } - HEAP32[$3 + 7296 >> 2] = 0; - $0 = HEAP32[$1 >> 2]; - HEAP32[$0 + 44 >> 2] = 13; - HEAP32[$0 + 48 >> 2] = 1056964608; - HEAP32[$0 + 36 >> 2] = 0; - HEAP32[$0 + 40 >> 2] = 1; - HEAP32[$0 + 28 >> 2] = 16; - HEAP32[$0 + 32 >> 2] = 44100; - HEAP32[$0 + 20 >> 2] = 0; - HEAP32[$0 + 24 >> 2] = 2; - HEAP32[$0 + 12 >> 2] = 1; - HEAP32[$0 + 16 >> 2] = 0; - HEAP32[$0 + 4 >> 2] = 0; - HEAP32[$0 + 8 >> 2] = 1; - $0 = HEAP32[$1 >> 2]; - HEAP32[$0 + 592 >> 2] = 0; - HEAP32[$0 + 596 >> 2] = 0; - HEAP32[$0 + 556 >> 2] = 0; - HEAP32[$0 + 560 >> 2] = 0; - HEAP32[$0 + 564 >> 2] = 0; - HEAP32[$0 + 568 >> 2] = 0; - HEAP32[$0 + 572 >> 2] = 0; - HEAP32[$0 + 576 >> 2] = 0; - HEAP32[$0 + 580 >> 2] = 0; - HEAP32[$0 + 584 >> 2] = 0; - HEAP32[$0 + 600 >> 2] = 0; - HEAP32[$0 + 604 >> 2] = 0; - $3 = HEAP32[$1 + 4 >> 2]; - $2 = $3; - HEAP32[$2 + 7248 >> 2] = 0; - HEAP32[$2 + 7252 >> 2] = 0; - HEAP32[$2 + 7048 >> 2] = 0; - $2 = $2 + 7256 | 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - $2 = $3 + 7264 | 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - $2 = $3 + 7272 | 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - $2 = $3 + 7280 | 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - HEAP32[$3 + 7288 >> 2] = 0; - FLAC__ogg_encoder_aspect_set_defaults($0 + 632 | 0); - $0 = HEAP32[$1 >> 2]; - label$5 : { - if (HEAP32[$0 >> 2] != 1) { - break label$5 - } - HEAP32[$0 + 16 >> 2] = 1; - HEAP32[$0 + 20 >> 2] = 0; - FLAC__stream_encoder_set_apodization($1, 10777); - $0 = HEAP32[$1 >> 2]; - if (HEAP32[$0 >> 2] != 1) { - break label$5 - } - HEAP32[$0 + 576 >> 2] = 0; - HEAP32[$0 + 580 >> 2] = 5; - HEAP32[$0 + 564 >> 2] = 0; - HEAP32[$0 + 568 >> 2] = 0; - HEAP32[$0 + 556 >> 2] = 8; - HEAP32[$0 + 560 >> 2] = 0; - } - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 11848 >> 2] = 0; - HEAP32[$0 + 6176 >> 2] = $0 + 336; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6180 >> 2] = $0 + 628; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6184 >> 2] = $0 + 920; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6188 >> 2] = $0 + 1212; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6192 >> 2] = $0 + 1504; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6196 >> 2] = $0 + 1796; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6200 >> 2] = $0 + 2088; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6204 >> 2] = $0 + 2380; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6208 >> 2] = $0 + 2672; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6212 >> 2] = $0 + 2964; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6216 >> 2] = $0 + 3256; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6220 >> 2] = $0 + 3548; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6224 >> 2] = $0 + 3840; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6228 >> 2] = $0 + 4132; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6232 >> 2] = $0 + 4424; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6236 >> 2] = $0 + 4716; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6240 >> 2] = $0 + 5008; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6244 >> 2] = $0 + 5300; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6248 >> 2] = $0 + 5592; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6252 >> 2] = $0 + 5884; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6640 >> 2] = $0 + 6256; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6644 >> 2] = $0 + 6268; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6648 >> 2] = $0 + 6280; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6652 >> 2] = $0 + 6292; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6656 >> 2] = $0 + 6304; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6660 >> 2] = $0 + 6316; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6664 >> 2] = $0 + 6328; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6668 >> 2] = $0 + 6340; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6672 >> 2] = $0 + 6352; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6676 >> 2] = $0 + 6364; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6680 >> 2] = $0 + 6376; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6684 >> 2] = $0 + 6388; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6688 >> 2] = $0 + 6400; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6692 >> 2] = $0 + 6412; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6696 >> 2] = $0 + 6424; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6700 >> 2] = $0 + 6436; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6704 >> 2] = $0 + 6448; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6708 >> 2] = $0 + 6460; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6712 >> 2] = $0 + 6472; - $0 = HEAP32[$1 + 4 >> 2]; - HEAP32[$0 + 6716 >> 2] = $0 + 6484; - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6256 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6268 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6280 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6292 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6304 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6316 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6328 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6340 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6352 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6364 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6376 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6388 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6400 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6412 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6424 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6436 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6448 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6460 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6472 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 6484 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 11724 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_init(HEAP32[$1 + 4 >> 2] + 11736 | 0); - HEAP32[HEAP32[$1 >> 2] >> 2] = 1; - return $1 | 0; - } - dlfree($1); - return 0; - } - - function FLAC__stream_encoder_set_apodization($0, $1) { - var $2 = 0, $3 = 0, $4 = 0, $5 = Math_fround(0), $6 = Math_fround(0), $7 = 0, $8 = 0.0, $9 = Math_fround(0), $10 = 0, $11 = 0; - $2 = HEAP32[$0 >> 2]; - label$1 : { - if (HEAP32[$2 >> 2] != 1) { - break label$1 - } - HEAP32[$2 + 40 >> 2] = 0; - while (1) { - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - label$8 : { - label$9 : { - label$10 : { - label$11 : { - label$12 : { - label$13 : { - label$14 : { - label$15 : { - label$16 : { - $10 = strchr($1, 59); - label$17 : { - if ($10) { - $4 = $10 - $1 | 0; - break label$17; - } - $4 = strlen($1); - } - $11 = ($4 | 0) != 8; - if (!$11) { - if (strncmp(10584, $1, 8)) { - break label$16 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 0; - break label$3; - } - label$20 : { - switch ($4 + -6 | 0) { - case 1: - break label$13; - case 0: - break label$14; - case 20: - break label$15; - case 7: - break label$20; - default: - break label$12; - }; - } - $7 = 1; - if (strncmp(10593, $1, 13)) { - break label$11 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 1; - break label$3; - } - $7 = 0; - if (strncmp(10607, $1, 8)) { - break label$11 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 2; - break label$3; - } - $7 = 0; - if (strncmp(10616, $1, 26)) { - break label$11 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 3; - break label$3; - } - if (strncmp(10643, $1, 6)) { - break label$3 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 4; - break label$3; - } - if (strncmp(10650, $1, 7)) { - break label$10 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 5; - break label$3; - } - $7 = 0; - if ($4 >>> 0 < 8) { - break label$9 - } - } - if (strncmp(10658, $1, 6)) { - break label$8 - } - $6 = Math_fround(strtod($1 + 6 | 0)); - if ($6 > Math_fround(0.0) ^ 1 | $6 <= Math_fround(.5) ^ 1) { - break label$3 - } - $1 = HEAP32[$0 >> 2]; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - $4 = HEAP32[$1 + 40 >> 2]; - HEAP32[$1 + 40 >> 2] = $4 + 1; - HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 6; - break label$3; - } - if (strncmp(10665, $1, 7)) { - break label$7 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 7; - break label$3; - } - label$21 : { - switch ($4 + -4 | 0) { - case 0: - break label$21; - case 1: - break label$5; - default: - break label$3; - }; - } - if (strncmp(10673, $1, 4)) { - break label$3 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 8; - break label$3; - } - if (!$7) { - break label$6 - } - if (strncmp(10678, $1, 13)) { - break label$6 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 9; - break label$3; - } - if (strncmp(10692, $1, 7)) { - break label$3 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 10; - break label$3; - } - label$22 : { - if (($4 | 0) != 9) { - break label$22 - } - if (strncmp(10700, $1, 9)) { - break label$22 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 11; - break label$3; - } - if (!$11) { - if (!strncmp(10710, $1, 8)) { - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 12; - break label$3; - } - if (strncmp(10719, $1, 6)) { - break label$3 - } - break label$4; - } - if (!strncmp(10719, $1, 6)) { - break label$4 - } - if ($4 >>> 0 < 16) { - break label$3 - } - if (!strncmp(10726, $1, 14)) { - $8 = strtod($1 + 14 | 0); - label$26 : { - if (Math_abs($8) < 2147483648.0) { - $4 = ~~$8; - break label$26; - } - $4 = -2147483648; - } - $3 = strchr($1, 47); - $5 = Math_fround(.10000000149011612); - label$28 : { - if (!$3) { - break label$28 - } - $2 = $3 + 1 | 0; - $5 = Math_fround(.9900000095367432); - if (!(Math_fround(strtod($2)) < Math_fround(.9900000095367432))) { - break label$28 - } - $5 = Math_fround(strtod($2)); - } - $1 = strchr($3 ? $3 + 1 | 0 : $1, 47); - $6 = Math_fround(.20000000298023224); - label$30 : { - if (!$1) { - break label$30 - } - $6 = Math_fround(strtod($1 + 1 | 0)); - } - $1 = HEAP32[$0 >> 2]; - $2 = HEAP32[$1 + 40 >> 2]; - if (($4 | 0) <= 1) { - HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - $4 = HEAP32[$1 + 40 >> 2]; - HEAP32[$1 + 40 >> 2] = $4 + 1; - HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; - break label$3; - } - if ($2 + $4 >>> 0 > 31) { - break label$3 - } - $9 = Math_fround(Math_fround(Math_fround(1.0) / Math_fround(Math_fround(1.0) - $5)) + Math_fround(-1.0)); - $5 = Math_fround($9 + Math_fround($4 | 0)); - $3 = 0; - while (1) { - HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 52 >> 2] = Math_fround($3 | 0) / $5; - $1 = HEAP32[$0 >> 2]; - $3 = $3 + 1 | 0; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 56 >> 2] = Math_fround($9 + Math_fround($3 | 0)) / $5; - $1 = HEAP32[$0 >> 2]; - $7 = HEAP32[$1 + 40 >> 2]; - $2 = $7 + 1 | 0; - HEAP32[$1 + 40 >> 2] = $2; - HEAP32[(($7 << 4) + $1 | 0) + 44 >> 2] = 14; - if (($3 | 0) != ($4 | 0)) { - continue - } - break; - }; - break label$3; - } - if ($4 >>> 0 < 17) { - break label$3 - } - if (strncmp(10741, $1, 15)) { - break label$3 - } - $8 = strtod($1 + 15 | 0); - label$33 : { - if (Math_abs($8) < 2147483648.0) { - $4 = ~~$8; - break label$33; - } - $4 = -2147483648; - } - $6 = Math_fround(.20000000298023224); - $3 = strchr($1, 47); - $5 = Math_fround(.20000000298023224); - label$35 : { - if (!$3) { - break label$35 - } - $2 = $3 + 1 | 0; - $5 = Math_fround(.9900000095367432); - if (!(Math_fround(strtod($2)) < Math_fround(.9900000095367432))) { - break label$35 - } - $5 = Math_fround(strtod($2)); - } - $1 = strchr($3 ? $3 + 1 | 0 : $1, 47); - if ($1) { - $6 = Math_fround(strtod($1 + 1 | 0)) - } - $1 = HEAP32[$0 >> 2]; - $2 = HEAP32[$1 + 40 >> 2]; - if (($4 | 0) <= 1) { - HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - $4 = HEAP32[$1 + 40 >> 2]; - HEAP32[$1 + 40 >> 2] = $4 + 1; - HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; - break label$3; - } - if ($2 + $4 >>> 0 > 31) { - break label$3 - } - $9 = Math_fround(Math_fround(Math_fround(1.0) / Math_fround(Math_fround(1.0) - $5)) + Math_fround(-1.0)); - $5 = Math_fround($9 + Math_fround($4 | 0)); - $3 = 0; - while (1) { - HEAPF32[(($2 << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 52 >> 2] = Math_fround($3 | 0) / $5; - $1 = HEAP32[$0 >> 2]; - $3 = $3 + 1 | 0; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 56 >> 2] = Math_fround($9 + Math_fround($3 | 0)) / $5; - $1 = HEAP32[$0 >> 2]; - $7 = HEAP32[$1 + 40 >> 2]; - $2 = $7 + 1 | 0; - HEAP32[$1 + 40 >> 2] = $2; - HEAP32[(($7 << 4) + $1 | 0) + 44 >> 2] = 15; - if (($3 | 0) != ($4 | 0)) { - continue - } - break; - }; - break label$3; - } - if (strncmp(10757, $1, 5)) { - break label$3 - } - HEAP32[$2 + 40 >> 2] = $3 + 1; - HEAP32[(($3 << 4) + $2 | 0) + 44 >> 2] = 16; - break label$3; - } - $6 = Math_fround(strtod($1 + 6 | 0)); - if ($6 >= Math_fround(0.0) ^ 1 | $6 <= Math_fround(1.0) ^ 1) { - break label$3 - } - $1 = HEAP32[$0 >> 2]; - HEAPF32[((HEAP32[$1 + 40 >> 2] << 4) + $1 | 0) + 48 >> 2] = $6; - $1 = HEAP32[$0 >> 2]; - $4 = HEAP32[$1 + 40 >> 2]; - HEAP32[$1 + 40 >> 2] = $4 + 1; - HEAP32[($1 + ($4 << 4) | 0) + 44 >> 2] = 13; - } - $2 = HEAP32[$0 >> 2]; - $3 = HEAP32[$2 + 40 >> 2]; - if ($10) { - $1 = $10 + 1 | 0; - if (($3 | 0) != 32) { - continue - } - } - break; - }; - $4 = 1; - if ($3) { - break label$1 - } - HEAP32[$2 + 40 >> 2] = 1; - HEAP32[$2 + 44 >> 2] = 13; - HEAP32[$2 + 48 >> 2] = 1056964608; - } - return $4; - } - - function FLAC__stream_encoder_delete($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0; - if ($0) { - HEAP32[HEAP32[$0 + 4 >> 2] + 11848 >> 2] = 1; - FLAC__stream_encoder_finish($0); - $1 = HEAP32[$0 + 4 >> 2]; - $2 = HEAP32[$1 + 11752 >> 2]; - if ($2) { - FLAC__stream_decoder_delete($2); - $1 = HEAP32[$0 + 4 >> 2]; - } - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear($1 + 6256 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6268 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6280 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6292 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6304 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6316 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6328 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6340 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6352 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6364 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6376 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6388 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6400 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6412 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6424 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6436 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6448 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6460 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6472 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 6484 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 11724 | 0); - FLAC__format_entropy_coding_method_partitioned_rice_contents_clear(HEAP32[$0 + 4 >> 2] + 11736 | 0); - FLAC__bitreader_delete(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); - dlfree(HEAP32[$0 + 4 >> 2]); - dlfree(HEAP32[$0 >> 2]); - dlfree($0); - } - } - - function FLAC__stream_encoder_finish($0) { - $0 = $0 | 0; - var $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; - $7 = global$0 - 32 | 0; - global$0 = $7; - label$1 : { - if (!$0) { - break label$1 - } - label$3 : { - label$4 : { - $5 = HEAP32[$0 >> 2]; - $1 = HEAP32[$5 >> 2]; - switch ($1 | 0) { - case 1: - break label$1; - case 0: - break label$4; - default: - break label$3; - }; - } - $2 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$2 + 11848 >> 2]) { - break label$3 - } - $2 = HEAP32[$2 + 7052 >> 2]; - if (!$2) { - break label$3 - } - $3 = HEAP32[$5 + 36 >> 2]; - HEAP32[$5 + 36 >> 2] = $2; - $3 = !process_frame_($0, ($2 | 0) != ($3 | 0), 1); - $5 = HEAP32[$0 >> 2]; - } - if (HEAP32[$5 + 12 >> 2]) { - $2 = HEAP32[$0 + 4 >> 2]; - FLAC__MD5Final($2 + 6928 | 0, $2 + 7060 | 0); - } - $5 = $0 + 4 | 0; - $1 = HEAP32[$0 + 4 >> 2]; - label$6 : { - if (HEAP32[$1 + 11848 >> 2]) { - $2 = $3; - break label$6; - } - $4 = HEAP32[$0 >> 2]; - label$8 : { - if (HEAP32[$4 >> 2]) { - break label$8 - } - $11 = HEAP32[$1 + 7268 >> 2]; - if ($11) { - label$10 : { - if (HEAP32[$1 + 7260 >> 2]) { - $13 = HEAP32[$1 + 6900 >> 2]; - $12 = HEAP32[$1 + 6896 >> 2]; - $2 = $1 + 6920 | 0; - $8 = HEAP32[$2 >> 2]; - $9 = HEAP32[$2 + 4 >> 2]; - if ((FUNCTION_TABLE[$11]($0, 0, 0, HEAP32[$1 + 7288 >> 2]) | 0) == 2) { - break label$10 - } - simple_ogg_page__init($7); - $2 = HEAP32[$0 >> 2]; - $4 = HEAP32[$2 + 608 >> 2]; - $6 = HEAP32[$2 + 612 >> 2]; - $2 = HEAP32[$0 + 4 >> 2]; - label$12 : { - if (!simple_ogg_page__get_at($0, $4, $6, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7264 >> 2], HEAP32[$2 + 7288 >> 2])) { - break label$12 - } - $11 = HEAP32[1357] + HEAP32[1356] | 0; - $14 = HEAP32[1362] + (HEAP32[1361] + (HEAP32[1360] + (HEAP32[1359] + ($11 + HEAP32[1358] | 0) | 0) | 0) | 0) | 0; - $2 = $14 + HEAP32[1363] >>> 3 | 0; - if ($2 + 33 >>> 0 > HEAPU32[$7 + 12 >> 2]) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - simple_ogg_page__clear($7); - break label$10; - } - $4 = $1 + 6936 | 0; - $10 = HEAPU8[$4 + 4 | 0] | HEAPU8[$4 + 5 | 0] << 8 | (HEAPU8[$4 + 6 | 0] << 16 | HEAPU8[$4 + 7 | 0] << 24); - $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; - $4 = HEAPU8[$4 | 0] | HEAPU8[$4 + 1 | 0] << 8 | (HEAPU8[$4 + 2 | 0] << 16 | HEAPU8[$4 + 3 | 0] << 24); - HEAP8[$2 + 25 | 0] = $4; - HEAP8[$2 + 26 | 0] = $4 >>> 8; - HEAP8[$2 + 27 | 0] = $4 >>> 16; - HEAP8[$2 + 28 | 0] = $4 >>> 24; - HEAP8[$2 + 29 | 0] = $10; - HEAP8[$2 + 30 | 0] = $10 >>> 8; - HEAP8[$2 + 31 | 0] = $10 >>> 16; - HEAP8[$2 + 32 | 0] = $10 >>> 24; - $1 = $1 + 6928 | 0; - $4 = HEAPU8[$1 + 4 | 0] | HEAPU8[$1 + 5 | 0] << 8 | (HEAPU8[$1 + 6 | 0] << 16 | HEAPU8[$1 + 7 | 0] << 24); - $1 = HEAPU8[$1 | 0] | HEAPU8[$1 + 1 | 0] << 8 | (HEAPU8[$1 + 2 | 0] << 16 | HEAPU8[$1 + 3 | 0] << 24); - HEAP8[$2 + 17 | 0] = $1; - HEAP8[$2 + 18 | 0] = $1 >>> 8; - HEAP8[$2 + 19 | 0] = $1 >>> 16; - HEAP8[$2 + 20 | 0] = $1 >>> 24; - HEAP8[$2 + 21 | 0] = $4; - HEAP8[$2 + 22 | 0] = $4 >>> 8; - HEAP8[$2 + 23 | 0] = $4 >>> 16; - HEAP8[$2 + 24 | 0] = $4 >>> 24; - $2 = $14 + -4 >>> 3 | 0; - if ($2 + 22 >>> 0 > HEAPU32[$7 + 12 >> 2]) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - simple_ogg_page__clear($7); - break label$10; - } - $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; - HEAP8[$2 + 21 | 0] = $8; - HEAP8[$2 + 20 | 0] = ($9 & 255) << 24 | $8 >>> 8; - HEAP8[$2 + 19 | 0] = ($9 & 65535) << 16 | $8 >>> 16; - HEAP8[$2 + 18 | 0] = ($9 & 16777215) << 8 | $8 >>> 24; - $2 = $2 + 17 | 0; - HEAP8[$2 | 0] = HEAPU8[$2 | 0] & 240 | $9 & 15; - $2 = $11 >>> 3 | 0; - if ($2 + 23 >>> 0 > HEAPU32[$7 + 12 >> 2]) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - simple_ogg_page__clear($7); - break label$10; - } - $2 = $2 + HEAP32[$7 + 8 >> 2] | 0; - HEAP8[$2 + 22 | 0] = $13; - HEAP8[$2 + 21 | 0] = $13 >>> 8; - HEAP8[$2 + 20 | 0] = $13 >>> 16; - HEAP8[$2 + 19 | 0] = $12; - HEAP8[$2 + 18 | 0] = $12 >>> 8; - HEAP8[$2 + 17 | 0] = $12 >>> 16; - $2 = HEAP32[$0 >> 2]; - $4 = HEAP32[$2 + 608 >> 2]; - $1 = HEAP32[$2 + 612 >> 2]; - $2 = HEAP32[$0 + 4 >> 2]; - $2 = simple_ogg_page__set_at($0, $4, $1, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7276 >> 2], HEAP32[$2 + 7288 >> 2]); - simple_ogg_page__clear($7); - if (!$2) { - break label$10 - } - $2 = HEAP32[HEAP32[$5 >> 2] + 7048 >> 2]; - if (!$2 | !HEAP32[$2 >> 2]) { - break label$10 - } - $1 = HEAP32[$0 >> 2]; - if (!(HEAP32[$1 + 616 >> 2] | HEAP32[$1 + 620 >> 2])) { - break label$10 - } - FLAC__format_seektable_sort($2); - simple_ogg_page__init($7); - $2 = HEAP32[$0 >> 2]; - $4 = HEAP32[$2 + 616 >> 2]; - $1 = HEAP32[$2 + 620 >> 2]; - $2 = HEAP32[$0 + 4 >> 2]; - if (!simple_ogg_page__get_at($0, $4, $1, $7, HEAP32[$2 + 7268 >> 2], HEAP32[$2 + 7264 >> 2], HEAP32[$2 + 7288 >> 2])) { - break label$12 - } - $6 = HEAP32[$5 >> 2]; - $2 = HEAP32[$6 + 7048 >> 2]; - $1 = HEAP32[$2 >> 2]; - if (HEAP32[$7 + 12 >> 2] != (Math_imul($1, 18) + 4 | 0)) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - simple_ogg_page__clear($7); - break label$10; - } - if ($1) { - $1 = HEAP32[$7 + 8 >> 2] + 4 | 0; - $4 = 0; - while (1) { - $8 = HEAP32[$2 + 4 >> 2] + Math_imul($4, 24) | 0; - $9 = HEAP32[$8 >> 2]; - $2 = HEAP32[$8 + 4 >> 2]; - $10 = HEAP32[$8 + 8 >> 2]; - $6 = HEAP32[$8 + 12 >> 2]; - $8 = HEAP32[$8 + 16 >> 2]; - HEAP8[$1 + 17 | 0] = $8; - HEAP8[$1 + 15 | 0] = $10; - HEAP8[$1 + 7 | 0] = $9; - HEAP8[$1 + 16 | 0] = $8 >>> 8; - HEAP8[$1 + 14 | 0] = ($6 & 255) << 24 | $10 >>> 8; - HEAP8[$1 + 13 | 0] = ($6 & 65535) << 16 | $10 >>> 16; - HEAP8[$1 + 12 | 0] = ($6 & 16777215) << 8 | $10 >>> 24; - HEAP8[$1 + 11 | 0] = $6; - HEAP8[$1 + 10 | 0] = $6 >>> 8; - HEAP8[$1 + 9 | 0] = $6 >>> 16; - HEAP8[$1 + 8 | 0] = $6 >>> 24; - HEAP8[$1 + 6 | 0] = ($2 & 255) << 24 | $9 >>> 8; - HEAP8[$1 + 5 | 0] = ($2 & 65535) << 16 | $9 >>> 16; - HEAP8[$1 + 4 | 0] = ($2 & 16777215) << 8 | $9 >>> 24; - HEAP8[$1 + 3 | 0] = $2; - HEAP8[$1 + 2 | 0] = $2 >>> 8; - HEAP8[$1 + 1 | 0] = $2 >>> 16; - HEAP8[$1 | 0] = $2 >>> 24; - $1 = $1 + 18 | 0; - $4 = $4 + 1 | 0; - $6 = HEAP32[$5 >> 2]; - $2 = HEAP32[$6 + 7048 >> 2]; - if ($4 >>> 0 < HEAPU32[$2 >> 2]) { - continue - } - break; - }; - } - $2 = HEAP32[$0 >> 2]; - simple_ogg_page__set_at($0, HEAP32[$2 + 616 >> 2], HEAP32[$2 + 620 >> 2], $7, HEAP32[$6 + 7268 >> 2], HEAP32[$6 + 7276 >> 2], HEAP32[$6 + 7288 >> 2]); - } - simple_ogg_page__clear($7); - break label$10; - } - $13 = HEAP32[$1 + 6912 >> 2]; - $8 = HEAP32[$1 + 6900 >> 2]; - $9 = HEAP32[$1 + 6896 >> 2]; - $6 = $1 + 6920 | 0; - $2 = HEAP32[$6 >> 2]; - $6 = HEAP32[$6 + 4 >> 2]; - label$19 : { - label$20 : { - $16 = $0; - $10 = HEAP32[$4 + 612 >> 2]; - $12 = HEAP32[1357] + HEAP32[1356] | 0; - $14 = HEAP32[1362] + (HEAP32[1361] + (HEAP32[1360] + (HEAP32[1359] + ($12 + HEAP32[1358] | 0) | 0) | 0) | 0) | 0; - $15 = ($14 + HEAP32[1363] >>> 3 | 0) + 4 | 0; - $4 = $15 + HEAP32[$4 + 608 >> 2] | 0; - if ($4 >>> 0 < $15 >>> 0) { - $10 = $10 + 1 | 0 - } - switch (FUNCTION_TABLE[$11]($16, $4, $10, HEAP32[$1 + 7288 >> 2]) | 0) { - case 0: - break label$19; - case 1: - break label$20; - default: - break label$10; - }; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - $4 = $1 + 6928 | 0; - $1 = HEAP32[$0 + 4 >> 2]; - if (FUNCTION_TABLE[HEAP32[$1 + 7276 >> 2]]($0, $4, 16, 0, 0, HEAP32[$1 + 7288 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - HEAP8[$7 + 4 | 0] = $2; - HEAP8[$7 + 3 | 0] = ($6 & 255) << 24 | $2 >>> 8; - HEAP8[$7 + 2 | 0] = ($6 & 65535) << 16 | $2 >>> 16; - HEAP8[$7 + 1 | 0] = ($6 & 16777215) << 8 | $2 >>> 24; - HEAP8[$7 | 0] = ($6 & 15 | $13 << 4) + 240; - label$22 : { - label$23 : { - $2 = ($14 + -4 >>> 3 | 0) + 4 | 0; - $1 = HEAP32[$0 >> 2]; - $4 = $2 + HEAP32[$1 + 608 >> 2] | 0; - $1 = HEAP32[$1 + 612 >> 2]; - $1 = $4 >>> 0 < $2 >>> 0 ? $1 + 1 | 0 : $1; - $2 = HEAP32[$0 + 4 >> 2]; - switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $4, $1, HEAP32[$2 + 7288 >> 2]) | 0) { - case 0: - break label$22; - case 1: - break label$23; - default: - break label$10; - }; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - $2 = HEAP32[$0 + 4 >> 2]; - if (FUNCTION_TABLE[HEAP32[$2 + 7276 >> 2]]($0, $7, 5, 0, 0, HEAP32[$2 + 7288 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - HEAP8[$7 + 5 | 0] = $8; - HEAP8[$7 + 4 | 0] = $8 >>> 8; - HEAP8[$7 + 3 | 0] = $8 >>> 16; - HEAP8[$7 + 2 | 0] = $9; - HEAP8[$7 + 1 | 0] = $9 >>> 8; - HEAP8[$7 | 0] = $9 >>> 16; - label$25 : { - label$26 : { - $2 = ($12 >>> 3 | 0) + 4 | 0; - $1 = HEAP32[$0 >> 2]; - $4 = $2 + HEAP32[$1 + 608 >> 2] | 0; - $1 = HEAP32[$1 + 612 >> 2]; - $1 = $4 >>> 0 < $2 >>> 0 ? $1 + 1 | 0 : $1; - $2 = HEAP32[$0 + 4 >> 2]; - switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $4, $1, HEAP32[$2 + 7288 >> 2]) | 0) { - case 0: - break label$25; - case 1: - break label$26; - default: - break label$10; - }; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - $2 = HEAP32[$0 + 4 >> 2]; - if (FUNCTION_TABLE[HEAP32[$2 + 7276 >> 2]]($0, $7, 6, 0, 0, HEAP32[$2 + 7288 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - $2 = HEAP32[HEAP32[$5 >> 2] + 7048 >> 2]; - if (!$2 | !HEAP32[$2 >> 2]) { - break label$10 - } - $1 = HEAP32[$0 >> 2]; - if (!(HEAP32[$1 + 616 >> 2] | HEAP32[$1 + 620 >> 2])) { - break label$10 - } - FLAC__format_seektable_sort($2); - label$28 : { - label$29 : { - label$30 : { - $2 = HEAP32[$0 >> 2]; - $1 = HEAP32[$2 + 616 >> 2] + 4 | 0; - $2 = HEAP32[$2 + 620 >> 2]; - $4 = $1 >>> 0 < 4 ? $2 + 1 | 0 : $2; - $2 = HEAP32[$0 + 4 >> 2]; - switch (FUNCTION_TABLE[HEAP32[$2 + 7268 >> 2]]($0, $1, $4, HEAP32[$2 + 7288 >> 2]) | 0) { - case 1: - break label$29; - case 0: - break label$30; - default: - break label$10; - }; - } - $4 = HEAP32[$5 >> 2]; - $1 = HEAP32[$4 + 7048 >> 2]; - if (!HEAP32[$1 >> 2]) { - break label$10 - } - $6 = 0; - break label$28; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - break label$10; - } - while (1) { - label$32 : { - $9 = Math_imul($6, 24); - $8 = $9 + HEAP32[$1 + 4 >> 2] | 0; - $2 = HEAP32[$8 + 4 >> 2]; - $8 = HEAP32[$8 >> 2]; - $10 = $8 << 24 | $8 << 8 & 16711680; - HEAP32[$7 >> 2] = (($2 & 255) << 24 | $8 >>> 8) & -16777216 | (($2 & 16777215) << 8 | $8 >>> 24) & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); - HEAP32[$7 + 4 >> 2] = ($2 << 24 | $8 >>> 8) & 65280 | ($2 << 8 | $8 >>> 24) & 255 | $10; - $8 = $9 + HEAP32[$1 + 4 >> 2] | 0; - $2 = HEAP32[$8 + 12 >> 2]; - $8 = HEAP32[$8 + 8 >> 2]; - $10 = $8 << 24 | $8 << 8 & 16711680; - HEAP32[$7 + 8 >> 2] = (($2 & 255) << 24 | $8 >>> 8) & -16777216 | (($2 & 16777215) << 8 | $8 >>> 24) & 16711680 | ($2 >>> 8 & 65280 | $2 >>> 24); - HEAP32[$7 + 12 >> 2] = ($2 << 24 | $8 >>> 8) & 65280 | ($2 << 8 | $8 >>> 24) & 255 | $10; - $2 = HEAPU16[($9 + HEAP32[$1 + 4 >> 2] | 0) + 16 >> 1]; - HEAP16[$7 + 16 >> 1] = ($2 << 24 | $2 << 8 & 16711680) >>> 16; - if (FUNCTION_TABLE[HEAP32[$4 + 7276 >> 2]]($0, $7, 18, 0, 0, HEAP32[$4 + 7288 >> 2])) { - break label$32 - } - $6 = $6 + 1 | 0; - $4 = HEAP32[$5 >> 2]; - $1 = HEAP32[$4 + 7048 >> 2]; - if ($6 >>> 0 < HEAPU32[$1 >> 2]) { - continue - } - break label$10; - } - break; - }; - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - } - $1 = HEAP32[$0 + 4 >> 2]; - $4 = HEAP32[$0 >> 2]; - $3 = HEAP32[$4 >> 2] ? 1 : $3; - } - $2 = HEAP32[$1 + 7280 >> 2]; - if (!$2) { - break label$8 - } - FUNCTION_TABLE[$2]($0, $1 + 6872 | 0, HEAP32[$1 + 7288 >> 2]); - $4 = HEAP32[$0 >> 2]; - } - if (!HEAP32[$4 + 4 >> 2]) { - $2 = $3; - break label$6; - } - $2 = HEAP32[HEAP32[$5 >> 2] + 11752 >> 2]; - if (!$2) { - $2 = $3; - break label$6; - } - if (FLAC__stream_decoder_finish($2)) { - $2 = $3; - break label$6; - } - $2 = 1; - if ($3) { - break label$6 - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 4; - } - $1 = HEAP32[$5 >> 2]; - $3 = HEAP32[$1 + 7296 >> 2]; - if ($3) { - if (($3 | 0) != HEAP32[1896]) { - fclose($3); - $1 = HEAP32[$5 >> 2]; - } - HEAP32[$1 + 7296 >> 2] = 0; - } - if (HEAP32[$1 + 7260 >> 2]) { - ogg_stream_clear(HEAP32[$0 >> 2] + 640 | 0) - } - $1 = HEAP32[$0 >> 2]; - $3 = HEAP32[$1 + 600 >> 2]; - if ($3) { - dlfree($3); - $1 = HEAP32[$0 >> 2]; - HEAP32[$1 + 600 >> 2] = 0; - HEAP32[$1 + 604 >> 2] = 0; - } - if (HEAP32[$1 + 24 >> 2]) { - $3 = 0; - while (1) { - $4 = HEAP32[$5 >> 2]; - $1 = $3 << 2; - $6 = HEAP32[($4 + $1 | 0) + 7328 >> 2]; - if ($6) { - dlfree($6); - HEAP32[($1 + HEAP32[$5 >> 2] | 0) + 7328 >> 2] = 0; - $4 = HEAP32[$5 >> 2]; - } - $4 = HEAP32[($4 + $1 | 0) + 7368 >> 2]; - if ($4) { - dlfree($4); - HEAP32[($1 + HEAP32[$5 >> 2] | 0) + 7368 >> 2] = 0; - } - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - }; - } - $1 = HEAP32[$5 >> 2]; - $3 = HEAP32[$1 + 7360 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7360 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7400 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7400 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7364 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7364 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7404 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7404 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $4 = HEAP32[$0 >> 2]; - if (HEAP32[$4 + 40 >> 2]) { - $3 = 0; - while (1) { - $6 = $3 << 2; - $8 = HEAP32[($6 + $1 | 0) + 7408 >> 2]; - if ($8) { - dlfree($8); - HEAP32[($6 + HEAP32[$0 + 4 >> 2] | 0) + 7408 >> 2] = 0; - $4 = HEAP32[$0 >> 2]; - $1 = HEAP32[$0 + 4 >> 2]; - } - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[$4 + 40 >> 2]) { - continue - } - break; - }; - } - $3 = HEAP32[$1 + 7536 >> 2]; - if ($3) { - dlfree($3); - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 7536 >> 2] = 0; - $4 = HEAP32[$0 >> 2]; - } - if (HEAP32[$4 + 24 >> 2]) { - $4 = 0; - while (1) { - $3 = $4 << 3; - $6 = HEAP32[($3 + $1 | 0) + 7540 >> 2]; - if ($6) { - dlfree($6); - HEAP32[($3 + HEAP32[$5 >> 2] | 0) + 7540 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $6 = HEAP32[($1 + $3 | 0) + 7544 >> 2]; - if ($6) { - dlfree($6); - HEAP32[($3 + HEAP32[$5 >> 2] | 0) + 7544 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $4 = $4 + 1 | 0; - if ($4 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - }; - } - $3 = HEAP32[$1 + 7604 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7604 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7608 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7608 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7612 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7612 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7616 >> 2]; - if ($3) { - dlfree($3); - HEAP32[HEAP32[$5 >> 2] + 7616 >> 2] = 0; - $1 = HEAP32[$5 >> 2]; - } - $3 = HEAP32[$1 + 7620 >> 2]; - if ($3) { - dlfree($3); - $1 = HEAP32[$5 >> 2]; - HEAP32[$1 + 7620 >> 2] = 0; - } - $3 = HEAP32[$1 + 7624 >> 2]; - if ($3) { - dlfree($3); - $1 = HEAP32[$5 >> 2]; - HEAP32[$1 + 7624 >> 2] = 0; - } - $3 = HEAP32[$0 >> 2]; - if (!(!HEAP32[$3 + 4 >> 2] | !HEAP32[$3 + 24 >> 2])) { - $5 = 0; - while (1) { - $4 = $5 << 2; - $6 = HEAP32[($4 + $1 | 0) + 11764 >> 2]; - if ($6) { - dlfree($6); - HEAP32[($4 + HEAP32[$0 + 4 >> 2] | 0) + 11764 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - $3 = HEAP32[$0 >> 2]; - } - $5 = $5 + 1 | 0; - if ($5 >>> 0 < HEAPU32[$3 + 24 >> 2]) { - continue - } - break; - }; - } - FLAC__bitwriter_free(HEAP32[$1 + 6856 >> 2]); - $3 = HEAP32[$0 >> 2]; - HEAP32[$3 + 44 >> 2] = 13; - HEAP32[$3 + 48 >> 2] = 1056964608; - HEAP32[$3 + 36 >> 2] = 0; - HEAP32[$3 + 40 >> 2] = 1; - HEAP32[$3 + 28 >> 2] = 16; - HEAP32[$3 + 32 >> 2] = 44100; - HEAP32[$3 + 20 >> 2] = 0; - HEAP32[$3 + 24 >> 2] = 2; - HEAP32[$3 + 12 >> 2] = 1; - HEAP32[$3 + 16 >> 2] = 0; - HEAP32[$3 + 4 >> 2] = 0; - HEAP32[$3 + 8 >> 2] = 1; - $3 = HEAP32[$0 >> 2]; - HEAP32[$3 + 592 >> 2] = 0; - HEAP32[$3 + 596 >> 2] = 0; - HEAP32[$3 + 556 >> 2] = 0; - HEAP32[$3 + 560 >> 2] = 0; - HEAP32[$3 + 564 >> 2] = 0; - HEAP32[$3 + 568 >> 2] = 0; - HEAP32[$3 + 572 >> 2] = 0; - HEAP32[$3 + 576 >> 2] = 0; - HEAP32[$3 + 580 >> 2] = 0; - HEAP32[$3 + 584 >> 2] = 0; - HEAP32[$3 + 600 >> 2] = 0; - HEAP32[$3 + 604 >> 2] = 0; - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 7248 >> 2] = 0; - HEAP32[$1 + 7252 >> 2] = 0; - HEAP32[$1 + 7048 >> 2] = 0; - $5 = $1 + 7256 | 0; - HEAP32[$5 >> 2] = 0; - HEAP32[$5 + 4 >> 2] = 0; - $5 = $1 + 7264 | 0; - HEAP32[$5 >> 2] = 0; - HEAP32[$5 + 4 >> 2] = 0; - $5 = $1 + 7272 | 0; - HEAP32[$5 >> 2] = 0; - HEAP32[$5 + 4 >> 2] = 0; - $5 = $1 + 7280 | 0; - HEAP32[$5 >> 2] = 0; - HEAP32[$5 + 4 >> 2] = 0; - HEAP32[$1 + 7288 >> 2] = 0; - FLAC__ogg_encoder_aspect_set_defaults($3 + 632 | 0); - $1 = HEAP32[$0 >> 2]; - label$74 : { - if (HEAP32[$1 >> 2] != 1) { - break label$74 - } - HEAP32[$1 + 16 >> 2] = 1; - HEAP32[$1 + 20 >> 2] = 0; - FLAC__stream_encoder_set_apodization($0, 10777); - $1 = HEAP32[$0 >> 2]; - if (HEAP32[$1 >> 2] != 1) { - break label$74 - } - HEAP32[$1 + 576 >> 2] = 0; - HEAP32[$1 + 580 >> 2] = 5; - HEAP32[$1 + 564 >> 2] = 0; - HEAP32[$1 + 568 >> 2] = 0; - HEAP32[$1 + 556 >> 2] = 8; - HEAP32[$1 + 560 >> 2] = 0; - } - if (!$2) { - HEAP32[$1 >> 2] = 1 - } - $1 = !$2; - } - global$0 = $7 + 32 | 0; - return $1 | 0; - } - - function process_frame_($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0; - $8 = global$0 - 48 | 0; - global$0 = $8; - label$1 : { - label$2 : { - $4 = HEAP32[$0 >> 2]; - if (!HEAP32[$4 + 12 >> 2]) { - break label$2 - } - $3 = HEAP32[$0 + 4 >> 2]; - $3 = FLAC__MD5Accumulate($3 + 7060 | 0, $3 + 4 | 0, HEAP32[$4 + 24 >> 2], HEAP32[$4 + 36 >> 2], HEAP32[$4 + 28 >> 2] + 7 >>> 3 | 0); - $4 = HEAP32[$0 >> 2]; - if ($3) { - break label$2 - } - HEAP32[$4 >> 2] = 8; - $1 = 0; - break label$1; - } - $3 = HEAP32[$4 + 576 >> 2]; - if ($1) { - $12 = 0 - } else { - $1 = FLAC__format_get_max_rice_partition_order_from_blocksize(HEAP32[$4 + 36 >> 2]); - $4 = HEAP32[$0 >> 2]; - $5 = HEAP32[$4 + 580 >> 2]; - $12 = $1 >>> 0 < $5 >>> 0 ? $1 : $5; - } - $7 = HEAP32[$4 + 36 >> 2]; - HEAP32[$8 + 8 >> 2] = $7; - HEAP32[$8 + 12 >> 2] = HEAP32[$4 + 32 >> 2]; - $1 = HEAP32[$4 + 24 >> 2]; - HEAP32[$8 + 20 >> 2] = 0; - HEAP32[$8 + 16 >> 2] = $1; - $1 = HEAP32[$4 + 28 >> 2]; - HEAP32[$8 + 28 >> 2] = 0; - HEAP32[$8 + 24 >> 2] = $1; - $5 = HEAP32[$0 + 4 >> 2]; - HEAP32[$8 + 32 >> 2] = HEAP32[$5 + 7056 >> 2]; - $14 = $3 >>> 0 < $12 >>> 0 ? $3 : $12; - label$5 : { - label$6 : { - label$7 : { - label$8 : { - label$9 : { - label$10 : { - label$11 : { - if (!HEAP32[$4 + 16 >> 2]) { - $10 = 1; - break label$11; - } - if (!HEAP32[$4 + 20 >> 2] | !HEAP32[$5 + 6864 >> 2]) { - break label$11 - } - $10 = 1; - $13 = 1; - if (HEAP32[$5 + 6868 >> 2]) { - break label$10 - } - } - label$13 : { - if (!HEAP32[$4 + 24 >> 2]) { - $3 = 0; - break label$13; - } - while (1) { - $13 = ($6 << 2) + $5 | 0; - $3 = 0; - $11 = 0; - label$16 : { - if (!$7) { - break label$16 - } - $15 = HEAP32[$13 + 4 >> 2]; - $1 = 0; - while (1) { - label$18 : { - $3 = HEAP32[$15 + ($1 << 2) >> 2] | $3; - $9 = $3 & 1; - $1 = $1 + 1 | 0; - if ($1 >>> 0 >= $7 >>> 0) { - break label$18 - } - if (!$9) { - continue - } - } - break; - }; - $1 = 0; - $11 = 0; - if (!$3) { - break label$16 - } - $11 = 0; - if ($9) { - break label$16 - } - while (1) { - $1 = $1 + 1 | 0; - $9 = $3 & 2; - $3 = $3 >> 1; - if (!$9) { - continue - } - break; - }; - $9 = 0; - $11 = 0; - if (!$1) { - break label$16 - } - while (1) { - $3 = $15 + ($9 << 2) | 0; - HEAP32[$3 >> 2] = HEAP32[$3 >> 2] >> $1; - $9 = $9 + 1 | 0; - if (($9 | 0) != ($7 | 0)) { - continue - } - break; - }; - $11 = $1; - } - $1 = $11; - $7 = Math_imul($6, 584) + $5 | 0; - $3 = HEAP32[$4 + 28 >> 2]; - $1 = $1 >>> 0 > $3 >>> 0 ? $3 : $1; - HEAP32[$7 + 624 >> 2] = $1; - HEAP32[$7 + 916 >> 2] = $1; - HEAP32[$13 + 216 >> 2] = $3 - $1; - $6 = $6 + 1 | 0; - $3 = HEAP32[$4 + 24 >> 2]; - if ($6 >>> 0 >= $3 >>> 0) { - break label$13 - } - $7 = HEAP32[$4 + 36 >> 2]; - continue; - }; - } - $1 = 1; - if ($10) { - break label$9 - } - $7 = HEAP32[$4 + 36 >> 2]; - $13 = 0; - } - $9 = HEAP32[$5 + 36 >> 2]; - $3 = 0; - $6 = 0; - label$21 : { - if (!$7) { - break label$21 - } - $1 = 0; - while (1) { - label$23 : { - $1 = HEAP32[($6 << 2) + $9 >> 2] | $1; - $10 = $1 & 1; - $6 = $6 + 1 | 0; - if ($6 >>> 0 >= $7 >>> 0) { - break label$23 - } - if (!$10) { - continue - } - } - break; - }; - $6 = 0; - if ($10 | !$1) { - break label$21 - } - while (1) { - $6 = $6 + 1 | 0; - $10 = $1 & 2; - $1 = $1 >> 1; - if (!$10) { - continue - } - break; - }; - $1 = 0; - if (!$6) { - $6 = 0; - break label$21; - } - while (1) { - $10 = ($1 << 2) + $9 | 0; - HEAP32[$10 >> 2] = HEAP32[$10 >> 2] >> $6; - $1 = $1 + 1 | 0; - if (($7 | 0) != ($1 | 0)) { - continue - } - break; - }; - } - $1 = HEAP32[$4 + 28 >> 2]; - $6 = $6 >>> 0 > $1 >>> 0 ? $1 : $6; - HEAP32[$5 + 5296 >> 2] = $6; - HEAP32[$5 + 5588 >> 2] = $6; - HEAP32[$5 + 248 >> 2] = $1 - $6; - $6 = HEAP32[$4 + 36 >> 2]; - label$27 : { - if (!$6) { - break label$27 - } - $7 = HEAP32[$5 + 40 >> 2]; - $1 = 0; - while (1) { - label$29 : { - $3 = HEAP32[$7 + ($1 << 2) >> 2] | $3; - $10 = $3 & 1; - $1 = $1 + 1 | 0; - if ($1 >>> 0 >= $6 >>> 0) { - break label$29 - } - if (!$10) { - continue - } - } - break; - }; - $1 = 0; - if (!$3) { - $3 = 0; - break label$27; - } - if ($10) { - $3 = 0; - break label$27; - } - while (1) { - $1 = $1 + 1 | 0; - $10 = $3 & 2; - $3 = $3 >> 1; - if (!$10) { - continue - } - break; - }; - $3 = 0; - if (!$1) { - break label$27 - } - while (1) { - $10 = $7 + ($3 << 2) | 0; - HEAP32[$10 >> 2] = HEAP32[$10 >> 2] >> $1; - $3 = $3 + 1 | 0; - if (($6 | 0) != ($3 | 0)) { - continue - } - break; - }; - $3 = $1; - } - $1 = HEAP32[$4 + 28 >> 2]; - $3 = $3 >>> 0 > $1 >>> 0 ? $1 : $3; - HEAP32[$5 + 5880 >> 2] = $3; - HEAP32[$5 + 6172 >> 2] = $3; - HEAP32[$5 + 252 >> 2] = ($1 - $3 | 0) + 1; - if ($13) { - break label$8 - } - $3 = HEAP32[$4 + 24 >> 2]; - $1 = 0; - } - $4 = $1; - if ($3) { - $3 = 0; - while (1) { - $1 = ($3 << 2) + $5 | 0; - $5 = ($3 << 3) + $5 | 0; - process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$1 + 216 >> 2], HEAP32[$1 + 4 >> 2], $5 + 6176 | 0, $5 + 6640 | 0, $5 + 256 | 0, $1 + 6768 | 0, $1 + 6808 | 0); - $5 = HEAP32[$0 + 4 >> 2]; - $3 = $3 + 1 | 0; - if ($3 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - }; - } - if ($4) { - break label$7 - } - $9 = HEAP32[$5 + 36 >> 2]; - } - process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$5 + 248 >> 2], $9, $5 + 6240 | 0, $5 + 6704 | 0, $5 + 320 | 0, $5 + 6800 | 0, $5 + 6840 | 0); - $1 = HEAP32[$0 + 4 >> 2]; - process_subframe_($0, $14, $12, $8 + 8 | 0, HEAP32[$1 + 252 >> 2], HEAP32[$1 + 40 >> 2], $1 + 6248 | 0, $1 + 6712 | 0, $1 + 328 | 0, $1 + 6804 | 0, $1 + 6844 | 0); - $11 = $8; - $1 = HEAP32[$0 + 4 >> 2]; - label$36 : { - if (!(!HEAP32[HEAP32[$0 >> 2] + 20 >> 2] | !HEAP32[$1 + 6864 >> 2])) { - $3 = HEAP32[$1 + 6868 >> 2] ? 3 : 0; - break label$36; - } - $3 = HEAP32[$1 + 6844 >> 2]; - $5 = HEAP32[$1 + 6808 >> 2]; - $4 = $3 + $5 | 0; - $6 = HEAP32[$1 + 6812 >> 2]; - $5 = $5 + $6 | 0; - $7 = $4 >>> 0 < $5 >>> 0; - $6 = $3 + $6 | 0; - $5 = $7 ? $4 : $5; - $4 = $6 >>> 0 < $5 >>> 0; - $3 = $3 + HEAP32[$1 + 6840 >> 2] >>> 0 < ($4 ? $6 : $5) >>> 0 ? 3 : $4 ? 2 : $7; - } - HEAP32[$11 + 20 >> 2] = $3; - if (!FLAC__frame_add_header($8 + 8 | 0, HEAP32[$1 + 6856 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - $1 = 0; - break label$1; - } - $5 = $0; - $6 = HEAP32[$8 + 8 >> 2]; - label$39 : { - label$40 : { - switch ($3 | 0) { - default: - $3 = HEAP32[$0 + 4 >> 2]; - $7 = 0; - $1 = 0; - $4 = 0; - $9 = 0; - break label$39; - case 0: - $3 = HEAP32[$0 + 4 >> 2]; - $4 = $3 + 336 | 0; - $1 = $4 + Math_imul(HEAP32[$3 + 6768 >> 2], 292) | 0; - $7 = ($4 + Math_imul(HEAP32[$3 + 6772 >> 2], 292) | 0) + 584 | 0; - $4 = HEAP32[$3 + 216 >> 2]; - $9 = HEAP32[$3 + 220 >> 2]; - break label$39; - case 1: - $3 = HEAP32[$0 + 4 >> 2]; - $1 = ($3 + Math_imul(HEAP32[$3 + 6768 >> 2], 292) | 0) + 336 | 0; - $7 = (Math_imul(HEAP32[$3 + 6804 >> 2], 292) + $3 | 0) + 5592 | 0; - $4 = HEAP32[$3 + 216 >> 2]; - $9 = HEAP32[$3 + 252 >> 2]; - break label$39; - case 2: - $3 = HEAP32[$0 + 4 >> 2]; - $7 = ($3 + Math_imul(HEAP32[$3 + 6772 >> 2], 292) | 0) + 920 | 0; - $1 = (Math_imul(HEAP32[$3 + 6804 >> 2], 292) + $3 | 0) + 5592 | 0; - $4 = HEAP32[$3 + 252 >> 2]; - $9 = HEAP32[$3 + 220 >> 2]; - break label$39; - case 3: - break label$40; - }; - } - $3 = HEAP32[$0 + 4 >> 2]; - $4 = $3 + 5008 | 0; - $1 = $4 + Math_imul(HEAP32[$3 + 6800 >> 2], 292) | 0; - $7 = ($4 + Math_imul(HEAP32[$3 + 6804 >> 2], 292) | 0) + 584 | 0; - $4 = HEAP32[$3 + 248 >> 2]; - $9 = HEAP32[$3 + 252 >> 2]; - } - if (!add_subframe_($5, $6, $4, $1, HEAP32[$3 + 6856 >> 2])) { - break label$6 - } - if (!add_subframe_($0, HEAP32[$8 + 8 >> 2], $9, $7, HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2])) { - break label$6 - } - $1 = HEAP32[$0 >> 2]; - break label$5; - } - $3 = FLAC__frame_add_header($8 + 8 | 0, HEAP32[$5 + 6856 >> 2]); - $1 = HEAP32[$0 >> 2]; - if ($3) { - if (!HEAP32[$1 + 24 >> 2]) { - break label$5 - } - $3 = 0; - while (1) { - $1 = HEAP32[$0 + 4 >> 2]; - $5 = $1 + ($3 << 2) | 0; - if (!add_subframe_($0, HEAP32[$8 + 8 >> 2], HEAP32[$5 + 216 >> 2], (($1 + Math_imul($3, 584) | 0) + Math_imul(HEAP32[$5 + 6768 >> 2], 292) | 0) + 336 | 0, HEAP32[$1 + 6856 >> 2])) { - break label$6 - } - $3 = $3 + 1 | 0; - $1 = HEAP32[$0 >> 2]; - if ($3 >>> 0 < HEAPU32[$1 + 24 >> 2]) { - continue - } - break; - }; - break label$5; - } - HEAP32[$1 >> 2] = 7; - } - $1 = 0; - break label$1; - } - if (HEAP32[$1 + 20 >> 2]) { - $1 = HEAP32[$0 + 4 >> 2]; - $3 = HEAP32[$1 + 6864 >> 2] + 1 | 0; - HEAP32[$1 + 6864 >> 2] = $3 >>> 0 < HEAPU32[$1 + 6860 >> 2] ? $3 : 0; - } - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 6868 >> 2] = HEAP32[$8 + 20 >> 2]; - $1 = HEAP32[$1 + 6856 >> 2]; - $3 = HEAP32[$1 + 16 >> 2] & 7; - $11 = 1; - __inlined_func$FLAC__bitwriter_zero_pad_to_byte_boundary : { - if (!$3) { - break __inlined_func$FLAC__bitwriter_zero_pad_to_byte_boundary - } - $11 = FLAC__bitwriter_write_zeroes($1, 8 - $3 | 0); - } - if (!$11) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $1 = 0; - break label$1; - } - label$49 : { - if (FLAC__bitwriter_get_write_crc16(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], $8 + 8 | 0)) { - if (FLAC__bitwriter_write_raw_uint32(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], HEAPU16[$8 + 8 >> 1], HEAP32[1404])) { - break label$49 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 8; - $1 = 0; - break label$1; - } - $1 = 0; - if (!write_bitbuffer_($0, HEAP32[HEAP32[$0 >> 2] + 36 >> 2], $2)) { - break label$1 - } - $1 = HEAP32[$0 + 4 >> 2]; - HEAP32[$1 + 7052 >> 2] = 0; - HEAP32[$1 + 7056 >> 2] = HEAP32[$1 + 7056 >> 2] + 1; - $2 = $1 + 6920 | 0; - $3 = $2; - $11 = $3; - $1 = HEAP32[$3 + 4 >> 2]; - $0 = HEAP32[HEAP32[$0 >> 2] + 36 >> 2]; - $2 = $0 + HEAP32[$3 >> 2] | 0; - if ($2 >>> 0 < $0 >>> 0) { - $1 = $1 + 1 | 0 - } - HEAP32[$11 >> 2] = $2; - HEAP32[$3 + 4 >> 2] = $1; - $1 = 1; - } - $0 = $1; - global$0 = $8 + 48 | 0; - return $0; - } - - function FLAC__stream_encoder_init_stream($0, $1, $2, $3, $4, $5) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - return init_stream_internal__1($0, 0, $1, $2, $3, $4, $5, 0) | 0; - } - - function init_stream_internal__1($0, $1, $2, $3, $4, $5, $6, $7) { - var $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0.0, $17 = 0, $18 = 0, $19 = 0; - $15 = global$0 - 176 | 0; - global$0 = $15; - $9 = 13; - $8 = HEAP32[$0 >> 2]; - label$1 : { - if (HEAP32[$8 >> 2] != 1) { - break label$1 - } - $9 = 3; - if (!$2 | ($4 ? 0 : $3)) { - break label$1 - } - $9 = 4; - $11 = HEAP32[$8 + 24 >> 2]; - if ($11 + -1 >>> 0 > 7) { - break label$1 - } - label$2 : { - label$3 : { - if (($11 | 0) != 2) { - HEAP32[$8 + 16 >> 2] = 0; - break label$3; - } - if (HEAP32[$8 + 16 >> 2]) { - break label$2 - } - } - HEAP32[$8 + 20 >> 2] = 0; - } - $11 = HEAP32[$8 + 28 >> 2]; - if ($11 >>> 0 >= 32) { - HEAP32[$8 + 16 >> 2] = 0; - $9 = 5; - break label$1; - } - $9 = 5; - if ($11 + -4 >>> 0 > 20) { - break label$1 - } - if (HEAP32[$8 + 32 >> 2] + -1 >>> 0 >= 655350) { - $9 = 6; - break label$1; - } - $8 = HEAP32[$0 >> 2]; - $10 = HEAP32[$8 + 36 >> 2]; - label$7 : { - if (!$10) { - $10 = HEAP32[$8 + 556 >> 2] ? 4096 : 1152; - HEAP32[$8 + 36 >> 2] = $10; - break label$7; - } - $9 = 7; - if ($10 + -16 >>> 0 > 65519) { - break label$1 - } - } - $9 = 8; - $11 = HEAP32[$8 + 556 >> 2]; - if ($11 >>> 0 > 32) { - break label$1 - } - $9 = 10; - if ($10 >>> 0 < $11 >>> 0) { - break label$1 - } - $11 = HEAP32[$8 + 560 >> 2]; - label$9 : { - if (!$11) { - $13 = $8; - $11 = HEAP32[$8 + 28 >> 2]; - label$11 : { - if ($11 >>> 0 <= 15) { - $11 = $11 >>> 0 > 5 ? ($11 >>> 1 | 0) + 2 | 0 : 5; - break label$11; - } - if (($11 | 0) == 16) { - $11 = 7; - if ($10 >>> 0 < 193) { - break label$11 - } - $11 = 8; - if ($10 >>> 0 < 385) { - break label$11 - } - $11 = 9; - if ($10 >>> 0 < 577) { - break label$11 - } - $11 = 10; - if ($10 >>> 0 < 1153) { - break label$11 - } - $11 = 11; - if ($10 >>> 0 < 2305) { - break label$11 - } - $11 = $10 >>> 0 < 4609 ? 12 : 13; - break label$11; - } - $11 = 13; - if ($10 >>> 0 < 385) { - break label$11 - } - $11 = $10 >>> 0 < 1153 ? 14 : 15; - } - HEAP32[$13 + 560 >> 2] = $11; - break label$9; - } - $9 = 9; - if ($11 + -5 >>> 0 > 10) { - break label$1 - } - } - label$14 : { - if (!HEAP32[$8 + 8 >> 2]) { - $10 = HEAP32[$8 + 580 >> 2]; - break label$14; - } - $9 = 11; - if (!(($10 >>> 0 < 4609 | HEAPU32[$8 + 32 >> 2] > 48e3) & $10 >>> 0 < 16385)) { - break label$1 - } - if (!FLAC__format_sample_rate_is_subset(HEAP32[HEAP32[$0 >> 2] + 32 >> 2])) { - break label$1 - } - $8 = HEAP32[$0 >> 2]; - if (__wasm_rotl_i32(HEAP32[$8 + 28 >> 2] + -8 | 0, 30) >>> 0 > 4) { - break label$1 - } - $10 = HEAP32[$8 + 580 >> 2]; - if ($10 >>> 0 > 8) { - break label$1 - } - if (HEAPU32[$8 + 32 >> 2] > 48e3) { - break label$14 - } - if (HEAPU32[$8 + 36 >> 2] > 4608 | HEAPU32[$8 + 556 >> 2] > 12) { - break label$1 - } - } - $11 = 1 << HEAP32[1406]; - if ($10 >>> 0 >= $11 >>> 0) { - $10 = $11 + -1 | 0; - HEAP32[$8 + 580 >> 2] = $10; - } - if (HEAPU32[$8 + 576 >> 2] >= $10 >>> 0) { - HEAP32[$8 + 576 >> 2] = $10 - } - label$18 : { - if (!$7) { - break label$18 - } - $10 = HEAP32[$8 + 600 >> 2]; - if (!$10) { - break label$18 - } - $13 = HEAP32[$8 + 604 >> 2]; - if ($13 >>> 0 < 2) { - break label$18 - } - $9 = 1; - while (1) { - $11 = HEAP32[($9 << 2) + $10 >> 2]; - if (!(!$11 | HEAP32[$11 >> 2] != 4)) { - while (1) { - $8 = ($9 << 2) + $10 | 0; - $9 = $9 + -1 | 0; - HEAP32[$8 >> 2] = HEAP32[($9 << 2) + $10 >> 2]; - $10 = HEAP32[HEAP32[$0 >> 2] + 600 >> 2]; - if ($9) { - continue - } - break; - }; - HEAP32[$10 >> 2] = $11; - $8 = HEAP32[$0 >> 2]; - break label$18; - } - $9 = $9 + 1 | 0; - if (($13 | 0) != ($9 | 0)) { - continue - } - break; - }; - } - $13 = HEAP32[$8 + 604 >> 2]; - label$22 : { - label$23 : { - $10 = HEAP32[$8 + 600 >> 2]; - if ($10) { - $11 = 0; - if (!$13) { - break label$22 - } - while (1) { - $8 = HEAP32[($11 << 2) + $10 >> 2]; - if (!(!$8 | HEAP32[$8 >> 2] != 3)) { - HEAP32[HEAP32[$0 + 4 >> 2] + 7048 >> 2] = $8 + 16; - break label$23; - } - $11 = $11 + 1 | 0; - if (($13 | 0) != ($11 | 0)) { - continue - } - break; - }; - break label$23; - } - $9 = 12; - if ($13) { - break label$1 - } - $11 = 0; - break label$22; - } - $8 = 0; - $13 = 0; - $11 = 0; - while (1) { - $9 = 12; - label$28 : { - label$29 : { - label$30 : { - label$31 : { - label$32 : { - $10 = HEAP32[($14 << 2) + $10 >> 2]; - switch (HEAP32[$10 >> 2]) { - case 0: - break label$1; - case 6: - break label$29; - case 5: - break label$30; - case 4: - break label$31; - case 3: - break label$32; - default: - break label$28; - }; - } - if ($18) { - break label$1 - } - $18 = 1; - $11 = $13; - $12 = $8; - if (FLAC__format_seektable_is_legal($10 + 16 | 0)) { - break label$28 - } - break label$1; - } - $11 = 1; - $12 = $8; - if (!$13) { - break label$28 - } - break label$1; - } - $11 = $13; - $12 = $8; - if (FLAC__format_cuesheet_is_legal($10 + 16 | 0, HEAP32[$10 + 160 >> 2])) { - break label$28 - } - break label$1; - } - $17 = $10 + 16 | 0; - if (!FLAC__format_picture_is_legal($17)) { - break label$1 - } - $11 = $13; - $12 = $8; - label$33 : { - switch (HEAP32[$17 >> 2] + -1 | 0) { - case 0: - if ($19) { - break label$1 - } - $12 = HEAP32[$10 + 20 >> 2]; - if (strcmp($12, 10763)) { - if (strcmp($12, 10773)) { - break label$1 - } - } - if (HEAP32[$10 + 28 >> 2] != 32) { - break label$1 - } - $19 = 1; - $11 = $13; - $12 = $8; - if (HEAP32[$10 + 32 >> 2] == 32) { - break label$28 - } - break label$1; - case 1: - break label$33; - default: - break label$28; - }; - } - $12 = 1; - if ($8) { - break label$1 - } - } - $14 = $14 + 1 | 0; - $8 = HEAP32[$0 >> 2]; - if ($14 >>> 0 >= HEAPU32[$8 + 604 >> 2]) { - break label$22 - } - $10 = HEAP32[$8 + 600 >> 2]; - $8 = $12; - $13 = $11; - continue; - }; - } - $10 = 0; - $14 = HEAP32[$0 + 4 >> 2]; - HEAP32[$14 >> 2] = 0; - if (HEAP32[$8 + 24 >> 2]) { - while (1) { - $8 = $10 << 2; - HEAP32[($8 + $14 | 0) + 4 >> 2] = 0; - HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 7328 >> 2] = 0; - HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 44 >> 2] = 0; - HEAP32[($8 + HEAP32[$0 + 4 >> 2] | 0) + 7368 >> 2] = 0; - $14 = HEAP32[$0 + 4 >> 2]; - $10 = $10 + 1 | 0; - if ($10 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - } - } - $8 = 0; - HEAP32[$14 + 36 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7360 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 76 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7400 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 40 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7364 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 80 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7404 >> 2] = 0; - $9 = HEAP32[$0 + 4 >> 2]; - $10 = HEAP32[$0 >> 2]; - if (HEAP32[$10 + 40 >> 2]) { - while (1) { - $12 = $8 << 2; - HEAP32[($12 + $9 | 0) + 84 >> 2] = 0; - HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7408 >> 2] = 0; - $9 = HEAP32[$0 + 4 >> 2]; - $8 = $8 + 1 | 0; - $10 = HEAP32[$0 >> 2]; - if ($8 >>> 0 < HEAPU32[$10 + 40 >> 2]) { - continue - } - break; - } - } - $8 = 0; - HEAP32[$9 + 7536 >> 2] = 0; - HEAP32[$9 + 212 >> 2] = 0; - if (HEAP32[$10 + 24 >> 2]) { - while (1) { - $12 = $8 << 3; - HEAP32[($12 + $9 | 0) + 256 >> 2] = 0; - HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7540 >> 2] = 0; - HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 260 >> 2] = 0; - HEAP32[($12 + HEAP32[$0 + 4 >> 2] | 0) + 7544 >> 2] = 0; - $9 = HEAP32[$0 + 4 >> 2]; - HEAP32[($9 + ($8 << 2) | 0) + 6768 >> 2] = 0; - $8 = $8 + 1 | 0; - if ($8 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - } - } - HEAP32[$9 + 320 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7604 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 324 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7608 >> 2] = 0; - $8 = HEAP32[$0 + 4 >> 2]; - HEAP32[$8 + 6800 >> 2] = 0; - HEAP32[$8 + 328 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7612 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 332 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 7616 >> 2] = 0; - $8 = HEAP32[$0 + 4 >> 2]; - HEAP32[$8 + 7620 >> 2] = 0; - HEAP32[$8 + 7624 >> 2] = 0; - HEAP32[$8 + 6848 >> 2] = 0; - HEAP32[$8 + 6852 >> 2] = 0; - HEAP32[$8 + 6804 >> 2] = 0; - $12 = HEAP32[$0 >> 2]; - $13 = HEAP32[$12 + 36 >> 2]; - $12 = HEAP32[$12 + 32 >> 2]; - HEAP32[$8 + 7052 >> 2] = 0; - HEAP32[$8 + 7056 >> 2] = 0; - HEAP32[$8 + 6864 >> 2] = 0; - $9 = $8; - $16 = +($12 >>> 0) * .4 / +($13 >>> 0) + .5; - label$42 : { - if ($16 < 4294967296.0 & $16 >= 0.0) { - $12 = ~~$16 >>> 0; - break label$42; - } - $12 = 0; - } - HEAP32[$9 + 6860 >> 2] = $12 ? $12 : 1; - FLAC__cpu_info($8 + 7156 | 0); - $9 = HEAP32[$0 + 4 >> 2]; - HEAP32[$9 + 7244 >> 2] = 12; - HEAP32[$9 + 7240 >> 2] = 13; - HEAP32[$9 + 7236 >> 2] = 12; - HEAP32[$9 + 7228 >> 2] = 14; - HEAP32[$9 + 7224 >> 2] = 15; - HEAP32[$9 + 7220 >> 2] = 16; - HEAP32[$9 + 7232 >> 2] = 17; - $10 = HEAP32[$0 >> 2]; - HEAP32[$10 >> 2] = 0; - HEAP32[$9 + 7260 >> 2] = $7; - label$44 : { - label$45 : { - label$46 : { - if ($7) { - if (!FLAC__ogg_encoder_aspect_init($10 + 632 | 0)) { - break label$46 - } - $10 = HEAP32[$0 >> 2]; - $9 = HEAP32[$0 + 4 >> 2]; - } - $8 = $0 + 4 | 0; - HEAP32[$9 + 7276 >> 2] = $2; - HEAP32[$9 + 7264 >> 2] = $1; - HEAP32[$9 + 7288 >> 2] = $6; - HEAP32[$9 + 7280 >> 2] = $5; - HEAP32[$9 + 7272 >> 2] = $4; - HEAP32[$9 + 7268 >> 2] = $3; - $1 = HEAP32[$10 + 36 >> 2]; - if (HEAPU32[$9 >> 2] < $1 >>> 0) { - $3 = $1 + 5 | 0; - label$49 : { - label$50 : { - label$51 : { - if (HEAP32[$10 + 24 >> 2]) { - $2 = 0; - while (1) { - $5 = $2 << 2; - $4 = $5 + HEAP32[$8 >> 2] | 0; - $6 = FLAC__memory_alloc_aligned_int32_array($3, $4 + 7328 | 0, $4 + 4 | 0); - $4 = HEAP32[($5 + HEAP32[$8 >> 2] | 0) + 4 >> 2]; - HEAP32[$4 >> 2] = 0; - HEAP32[$4 + 4 >> 2] = 0; - HEAP32[$4 + 8 >> 2] = 0; - HEAP32[$4 + 12 >> 2] = 0; - $4 = ($5 + HEAP32[$8 >> 2] | 0) + 4 | 0; - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + 16; - if (!$6) { - break label$51 - } - $2 = $2 + 1 | 0; - if ($2 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - continue - } - break; - }; - } - $2 = HEAP32[$8 >> 2]; - $4 = FLAC__memory_alloc_aligned_int32_array($3, $2 + 7360 | 0, $2 + 36 | 0); - $2 = HEAP32[HEAP32[$8 >> 2] + 36 >> 2]; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - HEAP32[$2 + 8 >> 2] = 0; - HEAP32[$2 + 12 >> 2] = 0; - $2 = HEAP32[$8 >> 2]; - HEAP32[$2 + 36 >> 2] = HEAP32[$2 + 36 >> 2] + 16; - if ($4) { - $2 = HEAP32[$8 >> 2]; - $3 = FLAC__memory_alloc_aligned_int32_array($3, $2 + 7364 | 0, $2 + 40 | 0); - $2 = HEAP32[HEAP32[$8 >> 2] + 40 >> 2]; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - HEAP32[$2 + 8 >> 2] = 0; - HEAP32[$2 + 12 >> 2] = 0; - $2 = HEAP32[$8 >> 2] + 40 | 0; - HEAP32[$2 >> 2] = HEAP32[$2 >> 2] + 16; - $2 = ($3 | 0) != 0; - } else { - $2 = ($4 | 0) != 0 - } - if (!$2) { - break label$51 - } - $3 = HEAP32[$0 >> 2]; - if (HEAP32[$3 + 556 >> 2]) { - $2 = HEAP32[$8 >> 2]; - if (HEAP32[$3 + 40 >> 2]) { - $9 = 0; - while (1) { - $2 = ($9 << 2) + $2 | 0; - if (!FLAC__memory_alloc_aligned_int32_array($1, $2 + 7408 | 0, $2 + 84 | 0)) { - break label$51 - } - $2 = HEAP32[$0 + 4 >> 2]; - $9 = $9 + 1 | 0; - if ($9 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 40 >> 2]) { - continue - } - break; - }; - } - if (!FLAC__memory_alloc_aligned_int32_array($1, $2 + 7536 | 0, $2 + 212 | 0)) { - break label$51 - } - } - $6 = 0; - $10 = 1; - $5 = 0; - while (1) { - if ($5 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 24 >> 2]) { - $9 = 0; - $2 = 1; - $3 = 0; - while (1) { - if ($9 & 1) { - break label$51 - } - $3 = (HEAP32[$8 >> 2] + ($5 << 3) | 0) + ($3 << 2) | 0; - $4 = FLAC__memory_alloc_aligned_int32_array($1, $3 + 7540 | 0, $3 + 256 | 0); - $7 = $2 & ($4 | 0) != 0; - $9 = !$4; - $3 = 1; - $2 = 0; - if ($7) { - continue - } - break; - }; - $5 = $5 + 1 | 0; - if ($4) { - continue - } - break label$51; - } - break; - }; - $7 = 1; - while (1) { - $9 = 0; - $2 = 1; - $3 = 0; - if (!$7) { - break label$51 - } - while (1) { - if ($9 & 1) { - break label$51 - } - $3 = (HEAP32[$8 >> 2] + ($6 << 3) | 0) + ($3 << 2) | 0; - $4 = FLAC__memory_alloc_aligned_int32_array($1, $3 + 7604 | 0, $3 + 320 | 0); - $5 = $2 & ($4 | 0) != 0; - $9 = !$4; - $3 = 1; - $2 = 0; - if ($5) { - continue - } - break; - }; - $7 = ($4 | 0) != 0; - $2 = $10 & $7; - $6 = 1; - $10 = 0; - if ($2) { - continue - } - break; - }; - if (!$4) { - break label$51 - } - $3 = $1 << 1; - $2 = HEAP32[$0 + 4 >> 2]; - $2 = FLAC__memory_alloc_aligned_uint64_array($3, $2 + 7620 | 0, $2 + 6848 | 0); - $9 = HEAP32[$0 >> 2]; - $4 = HEAP32[$9 + 572 >> 2]; - if (!$4 | !$2) { - break label$50 - } - $2 = HEAP32[$8 >> 2]; - if (FLAC__memory_alloc_aligned_int32_array($3, $2 + 7624 | 0, $2 + 6852 | 0)) { - break label$49 - } - } - $9 = HEAP32[$0 >> 2]; - break label$44; - } - if ($4 | !$2) { - break label$44 - } - } - $9 = HEAP32[$8 >> 2]; - label$64 : { - if (($1 | 0) == HEAP32[$9 >> 2]) { - break label$64 - } - $2 = HEAP32[$0 >> 2]; - if (!HEAP32[$2 + 556 >> 2] | !HEAP32[$2 + 40 >> 2]) { - break label$64 - } - $9 = 0; - while (1) { - label$66 : { - label$67 : { - label$68 : { - label$69 : { - label$70 : { - label$71 : { - label$72 : { - label$73 : { - label$74 : { - label$75 : { - label$76 : { - label$77 : { - label$78 : { - label$79 : { - label$80 : { - label$81 : { - label$82 : { - label$83 : { - label$84 : { - $2 = ($9 << 4) + $2 | 0; - switch (HEAP32[$2 + 44 >> 2]) { - case 16: - break label$68; - case 15: - break label$69; - case 14: - break label$70; - case 13: - break label$71; - case 12: - break label$72; - case 11: - break label$73; - case 10: - break label$74; - case 9: - break label$75; - case 8: - break label$76; - case 7: - break label$77; - case 6: - break label$78; - case 5: - break label$79; - case 4: - break label$80; - case 3: - break label$81; - case 2: - break label$82; - case 1: - break label$83; - case 0: - break label$84; - default: - break label$67; - }; - } - FLAC__window_bartlett(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_bartlett_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_blackman(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_blackman_harris_4term_92db_sidelobe(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_connes(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_flattop(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_gauss(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2]); - break label$66; - } - FLAC__window_hamming(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_kaiser_bessel(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_nuttall(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_rectangle(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_triangle(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2]); - break label$66; - } - FLAC__window_partial_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2], HEAPF32[$2 + 52 >> 2], HEAPF32[$2 + 56 >> 2]); - break label$66; - } - FLAC__window_punchout_tukey(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1, HEAPF32[$2 + 48 >> 2], HEAPF32[$2 + 52 >> 2], HEAPF32[$2 + 56 >> 2]); - break label$66; - } - FLAC__window_welch(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - break label$66; - } - FLAC__window_hann(HEAP32[(HEAP32[$8 >> 2] + ($9 << 2) | 0) + 84 >> 2], $1); - } - $9 = $9 + 1 | 0; - $2 = HEAP32[$0 >> 2]; - if ($9 >>> 0 < HEAPU32[$2 + 40 >> 2]) { - continue - } - break; - }; - $9 = HEAP32[$8 >> 2]; - } - HEAP32[$9 >> 2] = $1; - } - $1 = FLAC__bitwriter_init(HEAP32[$9 + 6856 >> 2]); - $3 = HEAP32[$0 >> 2]; - if (!$1) { - HEAP32[$3 >> 2] = 8; - $9 = 1; - break label$1; - } - if (HEAP32[$3 + 4 >> 2]) { - $9 = 1; - $2 = HEAP32[$8 >> 2]; - $1 = HEAP32[$3 + 36 >> 2] + 1 | 0; - HEAP32[$2 + 11796 >> 2] = $1; - label$87 : { - if (!HEAP32[$3 + 24 >> 2]) { - break label$87 - } - $1 = safe_malloc_mul_2op_p(4, $1); - HEAP32[HEAP32[$0 + 4 >> 2] + 11764 >> 2] = $1; - $3 = HEAP32[$0 >> 2]; - if ($1) { - while (1) { - $2 = HEAP32[$8 >> 2]; - if ($9 >>> 0 >= HEAPU32[$3 + 24 >> 2]) { - break label$87 - } - $1 = safe_malloc_mul_2op_p(4, HEAP32[$2 + 11796 >> 2]); - HEAP32[(HEAP32[$0 + 4 >> 2] + ($9 << 2) | 0) + 11764 >> 2] = $1; - $9 = $9 + 1 | 0; - $3 = HEAP32[$0 >> 2]; - if ($1) { - continue - } - break; - } - } - HEAP32[$3 >> 2] = 8; - $9 = 1; - break label$1; - } - HEAP32[$2 + 11800 >> 2] = 0; - label$90 : { - $2 = HEAP32[$2 + 11752 >> 2]; - if ($2) { - break label$90 - } - $2 = FLAC__stream_decoder_new(); - HEAP32[HEAP32[$8 >> 2] + 11752 >> 2] = $2; - if ($2) { - break label$90 - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 3; - $9 = 1; - break label$1; - } - $1 = FLAC__stream_decoder_init_stream($2, 18, 0, 0, 0, 0, 19, 20, 21, $0); - $3 = HEAP32[$0 >> 2]; - if ($1) { - break label$45 - } - $2 = !HEAP32[$3 + 4 >> 2]; - } else { - $2 = 1 - } - $1 = HEAP32[$8 >> 2]; - HEAP32[$1 + 7312 >> 2] = 0; - HEAP32[$1 + 7316 >> 2] = 0; - HEAP32[$1 + 7292 >> 2] = 0; - $4 = $1 + 11816 | 0; - HEAP32[$4 >> 2] = 0; - HEAP32[$4 + 4 >> 2] = 0; - $4 = $1 + 11824 | 0; - HEAP32[$4 >> 2] = 0; - HEAP32[$4 + 4 >> 2] = 0; - $4 = $1 + 11832 | 0; - HEAP32[$4 >> 2] = 0; - HEAP32[$4 + 4 >> 2] = 0; - HEAP32[$1 + 11840 >> 2] = 0; - HEAP32[$3 + 624 >> 2] = 0; - HEAP32[$3 + 628 >> 2] = 0; - HEAP32[$3 + 616 >> 2] = 0; - HEAP32[$3 + 620 >> 2] = 0; - HEAP32[$3 + 608 >> 2] = 0; - HEAP32[$3 + 612 >> 2] = 0; - if (!$2) { - HEAP32[$1 + 11756 >> 2] = 0 - } - if (!FLAC__bitwriter_write_raw_uint32(HEAP32[$1 + 6856 >> 2], HEAP32[1354], HEAP32[1355])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - $9 = 1; - break label$1; - } - $9 = 1; - if (!write_bitbuffer_($0, 0, 0)) { - break label$1 - } - $1 = HEAP32[$0 + 4 >> 2]; - $2 = HEAP32[$0 >> 2]; - if (HEAP32[$2 + 4 >> 2]) { - HEAP32[$1 + 11756 >> 2] = 1 - } - HEAP32[$1 + 6872 >> 2] = 0; - HEAP32[$1 + 6876 >> 2] = 0; - HEAP32[$1 + 6880 >> 2] = 34; - HEAP32[$1 + 6888 >> 2] = HEAP32[$2 + 36 >> 2]; - HEAP32[HEAP32[$0 + 4 >> 2] + 6892 >> 2] = HEAP32[HEAP32[$0 >> 2] + 36 >> 2]; - HEAP32[HEAP32[$0 + 4 >> 2] + 6896 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 6900 >> 2] = 0; - HEAP32[HEAP32[$0 + 4 >> 2] + 6904 >> 2] = HEAP32[HEAP32[$0 >> 2] + 32 >> 2]; - HEAP32[HEAP32[$0 + 4 >> 2] + 6908 >> 2] = HEAP32[HEAP32[$0 >> 2] + 24 >> 2]; - HEAP32[HEAP32[$0 + 4 >> 2] + 6912 >> 2] = HEAP32[HEAP32[$0 >> 2] + 28 >> 2]; - $1 = HEAP32[$0 >> 2]; - $2 = HEAP32[$1 + 596 >> 2]; - $3 = HEAP32[$0 + 4 >> 2] + 6920 | 0; - HEAP32[$3 >> 2] = HEAP32[$1 + 592 >> 2]; - HEAP32[$3 + 4 >> 2] = $2; - $1 = HEAP32[$0 + 4 >> 2]; - $2 = $1 + 6936 | 0; - HEAP32[$2 >> 2] = 0; - HEAP32[$2 + 4 >> 2] = 0; - $1 = $1 + 6928 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - if (HEAP32[HEAP32[$0 >> 2] + 12 >> 2]) { - FLAC__MD5Init(HEAP32[$8 >> 2] + 7060 | 0) - } - $1 = HEAP32[$8 >> 2]; - if (!FLAC__add_metadata_block($1 + 6872 | 0, HEAP32[$1 + 6856 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - break label$1; - } - if (!write_bitbuffer_($0, 0, 0)) { - break label$1 - } - HEAP32[HEAP32[$8 >> 2] + 6896 >> 2] = -1 << HEAP32[1358] ^ -1; - $1 = HEAP32[$8 >> 2] + 6920 | 0; - HEAP32[$1 >> 2] = 0; - HEAP32[$1 + 4 >> 2] = 0; - if (!$11) { - HEAP32[$15 >> 2] = 4; - $2 = HEAP32[HEAP32[$0 >> 2] + 604 >> 2]; - $1 = $15; - HEAP32[$1 + 24 >> 2] = 0; - HEAP32[$1 + 28 >> 2] = 0; - HEAP32[$1 + 16 >> 2] = 0; - HEAP32[$1 + 20 >> 2] = 0; - HEAP32[$1 + 8 >> 2] = 8; - HEAP32[$1 + 4 >> 2] = !$2; - if (!FLAC__add_metadata_block($1, HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - break label$1; - } - if (!write_bitbuffer_($0, 0, 0)) { - break label$1 - } - } - label$98 : { - $3 = HEAP32[$0 >> 2]; - $4 = HEAP32[$3 + 604 >> 2]; - if (!$4) { - break label$98 - } - $2 = 0; - while (1) { - $1 = HEAP32[HEAP32[$3 + 600 >> 2] + ($2 << 2) >> 2]; - HEAP32[$1 + 4 >> 2] = ($4 + -1 | 0) == ($2 | 0); - if (!FLAC__add_metadata_block($1, HEAP32[HEAP32[$8 >> 2] + 6856 >> 2])) { - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - break label$1; - } - if (write_bitbuffer_($0, 0, 0)) { - $2 = $2 + 1 | 0; - $3 = HEAP32[$0 >> 2]; - $4 = HEAP32[$3 + 604 >> 2]; - if ($2 >>> 0 >= $4 >>> 0) { - break label$98 - } - continue; - } - break; - }; - break label$1; - } - label$102 : { - $1 = HEAP32[$8 >> 2]; - $2 = HEAP32[$1 + 7272 >> 2]; - if (!$2) { - break label$102 - } - $1 = FUNCTION_TABLE[$2]($0, $3 + 624 | 0, HEAP32[$1 + 7288 >> 2]) | 0; - $3 = HEAP32[$0 >> 2]; - if (($1 | 0) != 1) { - break label$102 - } - HEAP32[$3 >> 2] = 5; - break label$1; - } - $9 = 0; - if (!HEAP32[$3 + 4 >> 2]) { - break label$1 - } - HEAP32[HEAP32[$8 >> 2] + 11756 >> 2] = 2; - break label$1; - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 2; - $9 = 1; - break label$1; - } - HEAP32[$3 >> 2] = 3; - $9 = 1; - break label$1; - } - HEAP32[$9 >> 2] = 8; - $9 = 1; - } - global$0 = $15 + 176 | 0; - return $9; - } - - function precompute_partition_info_sums_($0, $1, $2, $3, $4, $5, $6) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - var $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0; - $11 = 1 << $5; - $14 = $11 >>> 0 > 1 ? $11 : 1; - $8 = 0 - $3 | 0; - $12 = $2 + $3 >>> $5 | 0; - $9 = $12 - $3 | 0; - label$1 : { - if ($6 + 4 >>> 0 < (Math_clz32($12) ^ -32) + 33 >>> 0) { - $6 = 0; - while (1) { - $3 = 0; - $8 = $8 + $12 | 0; - if ($7 >>> 0 < $8 >>> 0) { - while (1) { - $2 = HEAP32[($7 << 2) + $0 >> 2]; - $10 = $2 >> 31; - $3 = ($10 ^ $2 + $10) + $3 | 0; - $7 = $7 + 1 | 0; - if ($7 >>> 0 < $8 >>> 0) { - continue - } - break; - }; - $7 = $9; - } - $2 = ($6 << 3) + $1 | 0; - HEAP32[$2 >> 2] = $3; - HEAP32[$2 + 4 >> 2] = 0; - $9 = $9 + $12 | 0; - $6 = $6 + 1 | 0; - if (($14 | 0) != ($6 | 0)) { - continue - } - break; - }; - break label$1; - } - $2 = 0; - while (1) { - $13 = 0; - $3 = 0; - $8 = $8 + $12 | 0; - if ($7 >>> 0 < $8 >>> 0) { - while (1) { - $6 = HEAP32[($7 << 2) + $0 >> 2]; - $10 = $6 >> 31; - $10 = $10 ^ $6 + $10; - $6 = $10 + $13 | 0; - if ($6 >>> 0 < $10 >>> 0) { - $3 = $3 + 1 | 0 - } - $13 = $6; - $7 = $7 + 1 | 0; - if ($7 >>> 0 < $8 >>> 0) { - continue - } - break; - }; - $7 = $9; - } - $6 = ($2 << 3) + $1 | 0; - HEAP32[$6 >> 2] = $13; - HEAP32[$6 + 4 >> 2] = $3; - $9 = $9 + $12 | 0; - $2 = $2 + 1 | 0; - if (($14 | 0) != ($2 | 0)) { - continue - } - break; - }; - } - if (($5 | 0) > ($4 | 0)) { - $7 = 0; - $0 = $11; - while (1) { - $5 = $5 + -1 | 0; - $8 = 0; - $0 = $0 >>> 1 | 0; - if ($0) { - while (1) { - $3 = ($7 << 3) + $1 | 0; - $2 = HEAP32[$3 + 8 >> 2]; - $9 = HEAP32[$3 + 12 >> 2] + HEAP32[$3 + 4 >> 2] | 0; - $3 = HEAP32[$3 >> 2]; - $2 = $3 + $2 | 0; - if ($2 >>> 0 < $3 >>> 0) { - $9 = $9 + 1 | 0 - } - $6 = ($11 << 3) + $1 | 0; - HEAP32[$6 >> 2] = $2; - HEAP32[$6 + 4 >> 2] = $9; - $7 = $7 + 2 | 0; - $11 = $11 + 1 | 0; - $8 = $8 + 1 | 0; - if (($8 | 0) != ($0 | 0)) { - continue - } - break; - } - } - if (($5 | 0) > ($4 | 0)) { - continue - } - break; - }; - } - } - - function verify_read_callback_($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - var $4 = 0, $5 = 0; - $5 = HEAP32[$3 + 4 >> 2]; - if (HEAP32[$5 + 11760 >> 2]) { - HEAP32[$2 >> 2] = 4; - $0 = HEAPU8[5409] | HEAPU8[5410] << 8 | (HEAPU8[5411] << 16 | HEAPU8[5412] << 24); - HEAP8[$1 | 0] = $0; - HEAP8[$1 + 1 | 0] = $0 >>> 8; - HEAP8[$1 + 2 | 0] = $0 >>> 16; - HEAP8[$1 + 3 | 0] = $0 >>> 24; - HEAP32[HEAP32[$3 + 4 >> 2] + 11760 >> 2] = 0; - return 0; - } - $0 = HEAP32[$5 + 11812 >> 2]; - if (!$0) { - return 2 - } - $4 = HEAP32[$2 >> 2]; - if ($0 >>> 0 < $4 >>> 0) { - HEAP32[$2 >> 2] = $0; - $4 = $0; - } - memcpy($1, HEAP32[$5 + 11804 >> 2], $4); - $0 = HEAP32[$3 + 4 >> 2]; - $1 = $0 + 11804 | 0; - $3 = $1; - $4 = HEAP32[$1 >> 2]; - $1 = HEAP32[$2 >> 2]; - HEAP32[$3 >> 2] = $4 + $1; - $0 = $0 + 11812 | 0; - HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - $1; - return 0; - } - - function verify_write_callback_($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, wasm2js_i32$0 = 0, wasm2js_i32$1 = 0; - $7 = HEAP32[$1 >> 2]; - $5 = HEAP32[$3 + 4 >> 2]; - $0 = HEAP32[$1 + 8 >> 2]; - if ($0) { - $4 = $7 << 2; - while (1) { - $8 = $6 << 2; - $9 = HEAP32[$8 + $2 >> 2]; - $10 = HEAP32[($5 + $8 | 0) + 11764 >> 2]; - if (memcmp($9, $10, $4)) { - $4 = 0; - label$4 : { - if ($7) { - $0 = 0; - while (1) { - $2 = $0 << 2; - $8 = HEAP32[$2 + $9 >> 2]; - $2 = HEAP32[$2 + $10 >> 2]; - if (($8 | 0) != ($2 | 0)) { - $4 = $0; - break label$4; - } - $0 = $0 + 1 | 0; - if (($7 | 0) != ($0 | 0)) { - continue - } - break; - }; - } - $2 = 0; - $8 = 0; - } - $9 = HEAP32[$1 + 28 >> 2]; - $0 = $4; - $11 = $0 + HEAP32[$1 + 24 >> 2] | 0; - if ($11 >>> 0 < $0 >>> 0) { - $9 = $9 + 1 | 0 - } - $10 = $5 + 11816 | 0; - HEAP32[$10 >> 2] = $11; - HEAP32[$10 + 4 >> 2] = $9; - $0 = HEAP32[$1 + 28 >> 2]; - $1 = HEAP32[$1 + 24 >> 2]; - HEAP32[$5 + 11840 >> 2] = $8; - HEAP32[$5 + 11836 >> 2] = $2; - HEAP32[$5 + 11832 >> 2] = $4; - HEAP32[$5 + 11828 >> 2] = $6; - (wasm2js_i32$0 = $5 + 11824 | 0, wasm2js_i32$1 = __wasm_i64_udiv($1, $0, $7)), HEAP32[wasm2js_i32$0 >> 2] = wasm2js_i32$1; - HEAP32[HEAP32[$3 >> 2] >> 2] = 4; - return 1; - } - $6 = $6 + 1 | 0; - if (($0 | 0) != ($6 | 0)) { - continue - } - break; - }; - $2 = $5 + 11800 | 0; - $1 = HEAP32[$2 >> 2] - $7 | 0; - HEAP32[$2 >> 2] = $1; - label$8 : { - if (!$0) { - break label$8 - } - $2 = HEAP32[$5 + 11764 >> 2]; - $4 = $2; - $2 = $7 << 2; - memmove($4, $4 + $2 | 0, $1 << 2); - $6 = 1; - if (($0 | 0) == 1) { - break label$8 - } - while (1) { - $1 = HEAP32[$3 + 4 >> 2]; - $4 = HEAP32[($1 + ($6 << 2) | 0) + 11764 >> 2]; - memmove($4, $2 + $4 | 0, HEAP32[$1 + 11800 >> 2] << 2); - $6 = $6 + 1 | 0; - if (($0 | 0) != ($6 | 0)) { - continue - } - break; - }; - } - return 0; - } - $0 = $5 + 11800 | 0; - HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - $7; - return 0; - } - - function verify_metadata_callback_($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - } - - function verify_error_callback_($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - HEAP32[HEAP32[$2 >> 2] >> 2] = 3; - } - - function write_bitbuffer_($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0; - $5 = global$0 - 16 | 0; - global$0 = $5; - $4 = FLAC__bitwriter_get_buffer(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2], $5 + 4 | 0, $5); - $3 = HEAP32[$0 >> 2]; - label$1 : { - label$2 : { - if (!$4) { - HEAP32[$3 >> 2] = 8; - break label$2; - } - label$4 : { - if (!HEAP32[$3 + 4 >> 2]) { - break label$4 - } - $3 = HEAP32[$0 + 4 >> 2]; - HEAP32[$3 + 11804 >> 2] = HEAP32[$5 + 4 >> 2]; - HEAP32[$3 + 11812 >> 2] = HEAP32[$5 >> 2]; - if (!HEAP32[$3 + 11756 >> 2]) { - HEAP32[$3 + 11760 >> 2] = 1; - break label$4; - } - if (FLAC__stream_decoder_process_single(HEAP32[$3 + 11752 >> 2])) { - break label$4 - } - FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 4) { - break label$1 - } - HEAP32[$0 >> 2] = 3; - break label$1; - } - $12 = HEAP32[$5 >> 2]; - $14 = HEAP32[$5 + 4 >> 2]; - HEAP32[$5 + 8 >> 2] = 0; - HEAP32[$5 + 12 >> 2] = 0; - label$6 : { - label$7 : { - $3 = HEAP32[$0 + 4 >> 2]; - $4 = HEAP32[$3 + 7272 >> 2]; - if (!$4) { - break label$7 - } - if ((FUNCTION_TABLE[$4]($0, $5 + 8 | 0, HEAP32[$3 + 7288 >> 2]) | 0) != 1) { - break label$7 - } - break label$6; - } - label$8 : { - if ($1) { - break label$8 - } - label$9 : { - switch (HEAPU8[$14 | 0] & 127) { - case 0: - $3 = HEAP32[$5 + 12 >> 2]; - $4 = HEAP32[$0 >> 2]; - HEAP32[$4 + 608 >> 2] = HEAP32[$5 + 8 >> 2]; - HEAP32[$4 + 612 >> 2] = $3; - break label$8; - case 3: - break label$9; - default: - break label$8; - }; - } - $3 = HEAP32[$0 >> 2]; - if (HEAP32[$3 + 616 >> 2] | HEAP32[$3 + 620 >> 2]) { - break label$8 - } - $4 = HEAP32[$5 + 12 >> 2]; - HEAP32[$3 + 616 >> 2] = HEAP32[$5 + 8 >> 2]; - HEAP32[$3 + 620 >> 2] = $4; - } - $6 = HEAP32[$0 + 4 >> 2]; - $7 = HEAP32[$6 + 7048 >> 2]; - label$11 : { - if (!$7) { - break label$11 - } - $8 = HEAP32[$0 >> 2]; - $4 = $8; - $3 = HEAP32[$4 + 628 >> 2]; - $15 = HEAP32[$4 + 624 >> 2]; - if (!($3 | $15)) { - break label$11 - } - $16 = HEAP32[$7 >> 2]; - if (!$16) { - break label$11 - } - $10 = HEAP32[$6 + 7292 >> 2]; - if ($10 >>> 0 >= $16 >>> 0) { - break label$11 - } - $13 = HEAP32[$6 + 7316 >> 2]; - $4 = $13; - $17 = HEAP32[$6 + 7312 >> 2]; - $18 = HEAP32[$8 + 36 >> 2]; - $8 = $18; - $9 = $17 + $8 | 0; - if ($9 >>> 0 < $8 >>> 0) { - $4 = $4 + 1 | 0 - } - $4 = $4 + -1 | 0; - $11 = $4 + 1 | 0; - $8 = $4; - $4 = $9 + -1 | 0; - $8 = ($4 | 0) != -1 ? $11 : $8; - $19 = HEAP32[$7 + 4 >> 2]; - while (1) { - $7 = $19 + Math_imul($10, 24) | 0; - $11 = HEAP32[$7 >> 2]; - $9 = HEAP32[$7 + 4 >> 2]; - if (($8 | 0) == ($9 | 0) & $11 >>> 0 > $4 >>> 0 | $9 >>> 0 > $8 >>> 0) { - break label$11 - } - if (($9 | 0) == ($13 | 0) & $11 >>> 0 >= $17 >>> 0 | $9 >>> 0 > $13 >>> 0) { - HEAP32[$7 >> 2] = $17; - HEAP32[$7 + 4 >> 2] = $13; - $9 = HEAP32[$5 + 8 >> 2]; - $11 = HEAP32[$5 + 12 >> 2]; - HEAP32[$7 + 16 >> 2] = $18; - HEAP32[$7 + 8 >> 2] = $9 - $15; - HEAP32[$7 + 12 >> 2] = $11 - ($3 + ($9 >>> 0 < $15 >>> 0) | 0); - } - $10 = $10 + 1 | 0; - HEAP32[$6 + 7292 >> 2] = $10; - if (($10 | 0) != ($16 | 0)) { - continue - } - break; - }; - } - label$14 : { - if (HEAP32[$6 + 7260 >> 2]) { - $2 = FLAC__ogg_encoder_aspect_write_callback_wrapper(HEAP32[$0 >> 2] + 632 | 0, $14, $12, $1, HEAP32[$6 + 7056 >> 2], $2, HEAP32[$6 + 7276 >> 2], $0, HEAP32[$6 + 7288 >> 2]); - break label$14; - } - $2 = FUNCTION_TABLE[HEAP32[$6 + 7276 >> 2]]($0, $14, $12, $1, HEAP32[$6 + 7056 >> 2], HEAP32[$6 + 7288 >> 2]) | 0; - } - if (!$2) { - $2 = HEAP32[$0 + 4 >> 2]; - $3 = $2; - $8 = $3; - $4 = HEAP32[$3 + 7308 >> 2]; - $6 = $12 + HEAP32[$3 + 7304 >> 2] | 0; - if ($6 >>> 0 < $12 >>> 0) { - $4 = $4 + 1 | 0 - } - HEAP32[$8 + 7304 >> 2] = $6; - HEAP32[$3 + 7308 >> 2] = $4; - $3 = HEAP32[$2 + 7316 >> 2]; - $4 = HEAP32[$2 + 7312 >> 2] + $1 | 0; - if ($4 >>> 0 < $1 >>> 0) { - $3 = $3 + 1 | 0 - } - HEAP32[$2 + 7312 >> 2] = $4; - HEAP32[$2 + 7316 >> 2] = $3; - $10 = 1; - $4 = $2; - $3 = HEAP32[$2 + 7320 >> 2]; - $2 = HEAP32[$2 + 7056 >> 2] + 1 | 0; - HEAP32[$4 + 7320 >> 2] = $3 >>> 0 > $2 >>> 0 ? $3 : $2; - FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); - if (!$1) { - break label$1 - } - $1 = HEAP32[$0 + 4 >> 2] + 6896 | 0; - $2 = HEAP32[$1 >> 2]; - $4 = $1; - $1 = HEAP32[$5 >> 2]; - HEAP32[$4 >> 2] = $1 >>> 0 < $2 >>> 0 ? $1 : $2; - $2 = HEAP32[$0 + 4 >> 2] + 6900 | 0; - $0 = HEAP32[$2 >> 2]; - HEAP32[$2 >> 2] = $1 >>> 0 > $0 >>> 0 ? $1 : $0; - break label$1; - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - FLAC__bitwriter_clear(HEAP32[HEAP32[$0 + 4 >> 2] + 6856 >> 2]); - HEAP32[HEAP32[$0 >> 2] >> 2] = 5; - } - $10 = 0; - } - global$0 = $5 + 16 | 0; - return $10; - } - - function FLAC__stream_encoder_init_ogg_stream($0, $1, $2, $3, $4, $5, $6) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - return init_stream_internal__1($0, $1, $2, $3, $4, $5, $6, 1) | 0; - } - - function process_subframe_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) { - var $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0.0, $25 = 0, $26 = 0, $27 = 0.0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = Math_fround(0), $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0, $41 = 0, $42 = Math_fround(0), $43 = 0, $44 = 0, $45 = 0, $46 = 0, $47 = 0, $48 = 0; - $14 = global$0 - 576 | 0; - global$0 = $14; - $25 = HEAP32[(HEAPU32[HEAP32[$0 >> 2] + 28 >> 2] > 16 ? 5644 : 5640) >> 2]; - $12 = HEAP32[$3 >> 2]; - label$1 : { - label$2 : { - if (HEAP32[HEAP32[$0 + 4 >> 2] + 7256 >> 2]) { - $11 = -1; - if ($12 >>> 0 > 3) { - break label$2 - } - } - $16 = HEAP32[$6 >> 2]; - HEAP32[$16 + 4 >> 2] = $5; - HEAP32[$16 >> 2] = 1; - $11 = HEAP32[$16 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + Math_imul($4, $12) | 0) | 0) | 0) | 0; - $12 = HEAP32[$3 >> 2]; - if ($12 >>> 0 < 4) { - break label$1 - } - } - $13 = HEAP32[$0 + 4 >> 2]; - $16 = $12 + -4 | 0; - label$4 : { - if (((Math_clz32($16 | 1) ^ 31) + $4 | 0) + 4 >>> 0 <= 32) { - $13 = FUNCTION_TABLE[HEAP32[$13 + 7224 >> 2]]($5 + 16 | 0, $16, $14 + 416 | 0) | 0; - break label$4; - } - $13 = FUNCTION_TABLE[HEAP32[$13 + 7228 >> 2]]($5 + 16 | 0, $16, $14 + 416 | 0) | 0; - } - label$6 : { - label$7 : { - label$8 : { - label$9 : { - $15 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$15 + 7248 >> 2] | HEAPF32[$14 + 420 >> 2] != Math_fround(0.0)) { - break label$9 - } - $12 = 1; - $17 = HEAP32[$5 >> 2]; - $16 = HEAP32[$3 >> 2]; - if ($16 >>> 0 <= 1) { - break label$8 - } - while (1) { - if (($17 | 0) != HEAP32[($12 << 2) + $5 >> 2]) { - break label$9 - } - $12 = $12 + 1 | 0; - if ($12 >>> 0 < $16 >>> 0) { - continue - } - break; - }; - break label$8; - } - $12 = HEAP32[$0 >> 2]; - if (!HEAP32[$15 + 7252 >> 2]) { - $16 = $11; - break label$7; - } - $16 = -1; - if (($11 | 0) != -1) { - $16 = $11; - break label$6; - } - if (!HEAP32[$12 + 556 >> 2]) { - break label$7 - } - $16 = $11; - break label$6; - } - $0 = HEAP32[$6 + 4 >> 2]; - HEAP32[$0 + 4 >> 2] = $17; - HEAP32[$0 >> 2] = 0; - $0 = HEAP32[$0 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + $4 | 0) | 0) | 0) | 0; - $19 = $0 >>> 0 < $11 >>> 0; - $11 = $19 ? $0 : $11; - break label$1; - } - $11 = HEAP32[$12 + 568 >> 2]; - $18 = $11 ? 0 : $13; - $13 = $11 ? 4 : $13; - $11 = HEAP32[$3 >> 2]; - $29 = $13 >>> 0 < $11 >>> 0 ? $13 : $11 + -1 | 0; - if ($18 >>> 0 > $29 >>> 0) { - break label$6 - } - $32 = $25 + -1 | 0; - $33 = HEAP32[1416]; - $30 = HEAP32[1415]; - $34 = HEAP32[1414]; - $42 = Math_fround($4 >>> 0); - while (1) { - $12 = $18 << 2; - $35 = HEAPF32[$12 + ($14 + 416 | 0) >> 2]; - if (!($35 >= $42)) { - $31 = !$19; - $17 = $31 << 2; - $36 = HEAP32[$17 + $7 >> 2]; - $21 = HEAP32[$6 + $17 >> 2]; - $23 = HEAP32[HEAP32[$0 >> 2] + 572 >> 2]; - $11 = HEAP32[$0 + 4 >> 2]; - $13 = HEAP32[$11 + 6852 >> 2]; - $15 = HEAP32[$11 + 6848 >> 2]; - $11 = $5 + $12 | 0; - $12 = HEAP32[$3 >> 2] - $18 | 0; - $17 = HEAP32[$8 + $17 >> 2]; - FLAC__fixed_compute_residual($11, $12, $18, $17); - HEAP32[$21 + 36 >> 2] = $17; - HEAP32[$21 + 12 >> 2] = $36; - HEAP32[$21 >> 2] = 2; - HEAP32[$21 + 4 >> 2] = 0; - $37 = $35 > Math_fround(0.0); - $26 = HEAP32[$0 + 4 >> 2]; - $22 = $18; - $27 = +$35 + .5; - label$15 : { - if ($27 < 4294967296.0 & $27 >= 0.0) { - $11 = ~~$27 >>> 0; - break label$15; - } - $11 = 0; - } - $11 = $37 ? $11 + 1 | 0 : 1; - $15 = find_best_partition_order_($26, $17, $15, $13, $12, $22, $11 >>> 0 < $25 >>> 0 ? $11 : $32, $25, $1, $2, $4, $23, $21 + 4 | 0); - HEAP32[$21 + 16 >> 2] = $18; - if ($18) { - $13 = $21 + 20 | 0; - $11 = 0; - while (1) { - $12 = $11 << 2; - HEAP32[$12 + $13 >> 2] = HEAP32[$5 + $12 >> 2]; - $11 = $11 + 1 | 0; - if (($18 | 0) != ($11 | 0)) { - continue - } - break; - }; - } - $11 = HEAP32[$21 + 288 >> 2] + ($33 + ($30 + ($34 + ($15 + Math_imul($4, $18) | 0) | 0) | 0) | 0) | 0; - $12 = $11 >>> 0 < $16 >>> 0; - $19 = $12 ? $31 : $19; - $16 = $12 ? $11 : $16; - } - $18 = $18 + 1 | 0; - if ($18 >>> 0 <= $29 >>> 0) { - continue - } - break; - }; - $12 = HEAP32[$0 >> 2]; - } - $13 = HEAP32[$12 + 556 >> 2]; - if (!$13) { - $11 = $16; - break label$1; - } - $11 = HEAP32[$3 >> 2]; - $13 = $13 >>> 0 < $11 >>> 0 ? $13 : $11 + -1 | 0; - HEAP32[$14 + 12 >> 2] = $13; - if (!$13) { - $11 = $16; - break label$1; - } - if (!HEAP32[$12 + 40 >> 2]) { - $11 = $16; - break label$1; - } - $40 = 33 - $4 | 0; - $43 = $25 + -1 | 0; - $44 = HEAP32[1413]; - $45 = HEAP32[1412]; - $46 = HEAP32[1416]; - $21 = HEAP32[1415]; - $47 = HEAP32[1414]; - $27 = +($4 >>> 0); - $29 = $4 >>> 0 < 18; - $32 = $4 >>> 0 > 16; - $33 = $4 >>> 0 > 17; - while (1) { - $12 = HEAP32[$0 + 4 >> 2]; - FLAC__lpc_window_data($5, HEAP32[($12 + ($38 << 2) | 0) + 84 >> 2], HEAP32[$12 + 212 >> 2], $11); - $11 = HEAP32[$0 + 4 >> 2]; - FUNCTION_TABLE[HEAP32[$11 + 7232 >> 2]](HEAP32[$11 + 212 >> 2], HEAP32[$3 >> 2], HEAP32[$14 + 12 >> 2] + 1 | 0, $14 + 272 | 0); - label$23 : { - if (HEAPF32[$14 + 272 >> 2] == Math_fround(0.0)) { - break label$23 - } - FLAC__lpc_compute_lp_coefficients($14 + 272 | 0, $14 + 12 | 0, HEAP32[$0 + 4 >> 2] + 7628 | 0, $14 + 16 | 0); - $15 = 1; - $12 = HEAP32[$14 + 12 >> 2]; - $17 = HEAP32[$0 >> 2]; - if (!HEAP32[$17 + 568 >> 2]) { - $11 = $14; - $12 = FLAC__lpc_compute_best_order($11 + 16 | 0, $12, HEAP32[$3 >> 2], (HEAP32[$17 + 564 >> 2] ? 5 : HEAP32[$17 + 560 >> 2]) + $4 | 0); - HEAP32[$11 + 12 >> 2] = $12; - $15 = $12; - } - $11 = HEAP32[$3 >> 2]; - if ($12 >>> 0 >= $11 >>> 0) { - $12 = $11 + -1 | 0; - HEAP32[$14 + 12 >> 2] = $12; - } - if ($15 >>> 0 > $12 >>> 0) { - break label$23 - } - while (1) { - label$29 : { - $30 = $15 + -1 | 0; - $24 = FLAC__lpc_compute_expected_bits_per_residual_sample(HEAPF64[($14 + 16 | 0) + ($30 << 3) >> 3], $11 - $15 | 0); - if ($24 >= $27) { - break label$29 - } - $11 = $24 > 0.0; - $24 = $24 + .5; - label$30 : { - if ($24 < 4294967296.0 & $24 >= 0.0) { - $13 = ~~$24 >>> 0; - break label$30; - } - $13 = 0; - } - $13 = $11 ? $13 + 1 | 0 : 1; - $11 = $13 >>> 0 < $25 >>> 0; - $12 = HEAP32[$0 >> 2]; - label$32 : { - if (HEAP32[$12 + 564 >> 2]) { - $22 = 5; - $26 = 15; - if ($33) { - break label$32 - } - $17 = (Math_clz32($15) ^ -32) + $40 | 0; - if ($17 >>> 0 > 14) { - break label$32 - } - $26 = $17 >>> 0 > 5 ? $17 : 5; - break label$32; - } - $26 = HEAP32[$12 + 560 >> 2]; - $22 = $26; - } - $34 = $11 ? $13 : $43; - $39 = ($15 << 2) + $5 | 0; - $11 = Math_clz32($15); - $31 = $11 ^ 31; - $41 = ($11 ^ -32) + $40 | 0; - while (1) { - $23 = HEAP32[$3 >> 2]; - $13 = !$19; - $11 = $13 << 2; - $37 = HEAP32[$11 + $7 >> 2]; - $20 = HEAP32[$6 + $11 >> 2]; - $28 = HEAP32[$8 + $11 >> 2]; - $36 = HEAP32[$12 + 572 >> 2]; - $12 = HEAP32[$0 + 4 >> 2]; - $18 = HEAP32[$12 + 6852 >> 2]; - $17 = HEAP32[$12 + 6848 >> 2]; - $11 = 0; - $48 = $19; - $19 = ($12 + ($30 << 7) | 0) + 7628 | 0; - $12 = $29 ? ($41 >>> 0 > $22 >>> 0 ? $22 : $41) : $22; - if (!FLAC__lpc_quantize_coefficients($19, $15, $12, $14 + 448 | 0, $14 + 444 | 0)) { - $23 = $23 - $15 | 0; - $19 = $4 + $12 | 0; - label$37 : { - if ($19 + $31 >>> 0 <= 32) { - $11 = HEAP32[$0 + 4 >> 2]; - if (!($12 >>> 0 > 16 | $32)) { - FUNCTION_TABLE[HEAP32[$11 + 7244 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); - break label$37; - } - FUNCTION_TABLE[HEAP32[$11 + 7236 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); - break label$37; - } - FUNCTION_TABLE[HEAP32[HEAP32[$0 + 4 >> 2] + 7240 >> 2]]($39, $23, $14 + 448 | 0, $15, HEAP32[$14 + 444 >> 2], $28); - } - HEAP32[$20 >> 2] = 3; - HEAP32[$20 + 4 >> 2] = 0; - HEAP32[$20 + 284 >> 2] = $28; - HEAP32[$20 + 12 >> 2] = $37; - $18 = find_best_partition_order_(HEAP32[$0 + 4 >> 2], $28, $17, $18, $23, $15, $34, $25, $1, $2, $4, $36, $20 + 4 | 0); - HEAP32[$20 + 20 >> 2] = $12; - HEAP32[$20 + 16 >> 2] = $15; - HEAP32[$20 + 24 >> 2] = HEAP32[$14 + 444 >> 2]; - memcpy($20 + 28 | 0, $14 + 448 | 0, 128); - $11 = 0; - if ($15) { - while (1) { - $17 = $11 << 2; - HEAP32[($17 + $20 | 0) + 156 >> 2] = HEAP32[$5 + $17 >> 2]; - $11 = $11 + 1 | 0; - if (($15 | 0) != ($11 | 0)) { - continue - } - break; - } - } - $11 = ((HEAP32[$20 + 288 >> 2] + (((($18 + Math_imul($15, $19) | 0) + $47 | 0) + $21 | 0) + $46 | 0) | 0) + $45 | 0) + $44 | 0; - } - $12 = ($11 | 0) != 0 & $11 >>> 0 < $16 >>> 0; - $19 = $12 ? $13 : $48; - $16 = $12 ? $11 : $16; - $22 = $22 + 1 | 0; - if ($22 >>> 0 > $26 >>> 0) { - break label$29 - } - $12 = HEAP32[$0 >> 2]; - continue; - }; - } - $15 = $15 + 1 | 0; - if ($15 >>> 0 > HEAPU32[$14 + 12 >> 2]) { - break label$23 - } - $11 = HEAP32[$3 >> 2]; - continue; - }; - } - $38 = $38 + 1 | 0; - if ($38 >>> 0 < HEAPU32[HEAP32[$0 >> 2] + 40 >> 2]) { - $11 = HEAP32[$3 >> 2]; - continue; - } - break; - }; - $11 = $16; - } - if (($11 | 0) == -1) { - $0 = HEAP32[$3 >> 2]; - $1 = HEAP32[($19 << 2) + $6 >> 2]; - HEAP32[$1 + 4 >> 2] = $5; - HEAP32[$1 >> 2] = 1; - $11 = HEAP32[$1 + 288 >> 2] + (HEAP32[1416] + (HEAP32[1415] + (HEAP32[1414] + Math_imul($0, $4) | 0) | 0) | 0) | 0; - } - HEAP32[$9 >> 2] = $19; - HEAP32[$10 >> 2] = $11; - global$0 = $14 + 576 | 0; - } - - function add_subframe_($0, $1, $2, $3, $4) { - var $5 = 0; - $5 = 1; - label$1 : { - label$2 : { - label$3 : { - switch (HEAP32[$3 >> 2]) { - case 0: - if (FLAC__subframe_add_constant($3 + 4 | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { - break label$1 - } - break label$2; - case 2: - if (FLAC__subframe_add_fixed($3 + 4 | 0, $1 - HEAP32[$3 + 16 >> 2] | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { - break label$1 - } - break label$2; - case 3: - if (FLAC__subframe_add_lpc($3 + 4 | 0, $1 - HEAP32[$3 + 16 >> 2] | 0, $2, HEAP32[$3 + 288 >> 2], $4)) { - break label$1 - } - break label$2; - case 1: - break label$3; - default: - break label$1; - }; - } - if (FLAC__subframe_add_verbatim($3 + 4 | 0, $1, $2, HEAP32[$3 + 288 >> 2], $4)) { - break label$1 - } - } - HEAP32[HEAP32[$0 >> 2] >> 2] = 7; - $5 = 0; - } - return $5; - } - - function FLAC__stream_encoder_set_ogg_serial_number($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 632 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_verify($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 4 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_channels($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 24 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_bits_per_sample($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 28 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_sample_rate($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 32 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_compression_level($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - var $2 = 0, $3 = 0, $4 = 0; - $3 = HEAP32[$0 >> 2]; - if (HEAP32[$3 >> 2] == 1) { - $2 = Math_imul($1 >>> 0 < 8 ? $1 : 8, 44); - $1 = $2 + 11184 | 0; - $4 = HEAP32[$1 + 4 >> 2]; - HEAP32[$3 + 16 >> 2] = HEAP32[$1 >> 2]; - HEAP32[$3 + 20 >> 2] = $4; - $3 = FLAC__stream_encoder_set_apodization($0, HEAP32[$1 + 40 >> 2]); - $1 = 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - $1 = $2 + 11184 | 0; - $2 = HEAP32[$1 + 32 >> 2]; - HEAP32[$0 + 576 >> 2] = HEAP32[$1 + 28 >> 2]; - HEAP32[$0 + 580 >> 2] = $2; - HEAP32[$0 + 568 >> 2] = HEAP32[$1 + 24 >> 2]; - HEAP32[$0 + 564 >> 2] = HEAP32[$1 + 16 >> 2]; - $2 = HEAP32[$1 + 12 >> 2]; - HEAP32[$0 + 556 >> 2] = HEAP32[$1 + 8 >> 2]; - HEAP32[$0 + 560 >> 2] = $2; - $1 = $3 & 1; - $0 = 1; - } else { - $0 = 0 - } - $0 = $0 & $1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_blocksize($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - HEAP32[$0 + 36 >> 2] = $1; - $0 = 1; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0; - $0 = HEAP32[$0 >> 2]; - if (HEAP32[$0 >> 2] == 1) { - $6 = $2; - $7 = $0; - $8 = $1; - $4 = HEAP32[1363]; - $3 = $4 & 31; - if (32 <= ($4 & 63) >>> 0) { - $4 = -1 << $3; - $3 = 0; - } else { - $4 = (1 << $3) - 1 & -1 >>> 32 - $3 | -1 << $3; - $3 = -1 << $3; - } - $5 = $3 ^ -1; - $3 = $4 ^ -1; - $1 = ($2 | 0) == ($3 | 0) & $5 >>> 0 > $1 >>> 0 | $3 >>> 0 > $2 >>> 0; - HEAP32[$7 + 592 >> 2] = $1 ? $8 : $5; - HEAP32[$0 + 596 >> 2] = $1 ? $6 : $3; - $0 = 1; - } else { - $0 = 0 - } - return $0; - } - - function FLAC__stream_encoder_set_metadata($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0; - $3 = HEAP32[$0 >> 2]; - if (HEAP32[$3 >> 2] == 1) { - $4 = HEAP32[$3 + 600 >> 2]; - if ($4) { - dlfree($4); - $3 = HEAP32[$0 >> 2]; - HEAP32[$3 + 600 >> 2] = 0; - HEAP32[$3 + 604 >> 2] = 0; - } - $2 = $1 ? $2 : 0; - if ($2) { - $3 = safe_malloc_mul_2op_p(4, $2); - if (!$3) { - return 0 - } - $1 = memcpy($3, $1, $2 << 2); - $3 = HEAP32[$0 >> 2]; - HEAP32[$3 + 604 >> 2] = $2; - HEAP32[$3 + 600 >> 2] = $1; - } - $0 = $3 + 632 | 0; - if ($2 >>> HEAP32[1886]) { - $0 = 0 - } else { - HEAP32[$0 + 4 >> 2] = $2; - $0 = 1; - } - $0 = ($0 | 0) != 0; - } else { - $0 = 0 - } - return $0 | 0; - } - - function FLAC__stream_encoder_get_verify_decoder_state($0) { - $0 = $0 | 0; - if (!HEAP32[HEAP32[$0 >> 2] + 4 >> 2]) { - return 9 - } - return FLAC__stream_decoder_get_state(HEAP32[HEAP32[$0 + 4 >> 2] + 11752 >> 2]) | 0; - } - - function FLAC__stream_encoder_get_verify($0) { - $0 = $0 | 0; - return HEAP32[HEAP32[$0 >> 2] + 4 >> 2]; - } - - function FLAC__stream_encoder_process($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0; - $5 = HEAP32[$0 >> 2]; - $11 = HEAP32[$5 + 36 >> 2]; - $16 = $11 + 1 | 0; - $4 = HEAP32[$0 + 4 >> 2]; - $10 = HEAP32[$5 + 24 >> 2]; - $13 = $11 << 2; - label$1 : { - while (1) { - $3 = $16 - HEAP32[$4 + 7052 >> 2] | 0; - $6 = $2 - $7 | 0; - $6 = $3 >>> 0 < $6 >>> 0 ? $3 : $6; - if (HEAP32[$5 + 4 >> 2]) { - if ($10) { - $5 = $6 << 2; - $3 = 0; - while (1) { - $8 = $3 << 2; - memcpy(HEAP32[($8 + $4 | 0) + 11764 >> 2] + (HEAP32[$4 + 11800 >> 2] << 2) | 0, HEAP32[$1 + $8 >> 2] + ($7 << 2) | 0, $5); - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - $4 = $4 + 11800 | 0; - HEAP32[$4 >> 2] = HEAP32[$4 >> 2] + $6; - } - if ($10) { - $5 = $6 << 2; - $4 = 0; - $3 = 0; - while (1) { - $8 = $3 << 2; - $12 = HEAP32[$8 + $1 >> 2]; - if (!$12) { - break label$1 - } - $9 = $8; - $8 = HEAP32[$0 + 4 >> 2]; - memcpy(HEAP32[($9 + $8 | 0) + 4 >> 2] + (HEAP32[$8 + 7052 >> 2] << 2) | 0, $12 + ($7 << 2) | 0, $5); - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - $5 = HEAP32[$0 >> 2]; - label$8 : { - if (HEAP32[$5 + 16 >> 2]) { - $4 = HEAP32[$0 + 4 >> 2]; - if ($7 >>> 0 >= $2 >>> 0) { - break label$8 - } - $3 = HEAP32[$4 + 7052 >> 2]; - if ($3 >>> 0 > $11 >>> 0) { - break label$8 - } - $8 = HEAP32[$4 + 40 >> 2]; - $12 = HEAP32[$4 + 36 >> 2]; - $17 = HEAP32[$1 + 4 >> 2]; - $18 = HEAP32[$1 >> 2]; - while (1) { - $14 = $3 << 2; - $9 = $7 << 2; - $15 = $9 + $18 | 0; - $9 = $9 + $17 | 0; - HEAP32[$14 + $8 >> 2] = HEAP32[$15 >> 2] - HEAP32[$9 >> 2]; - HEAP32[$12 + $14 >> 2] = HEAP32[$9 >> 2] + HEAP32[$15 >> 2] >> 1; - $7 = $7 + 1 | 0; - if ($7 >>> 0 >= $2 >>> 0) { - break label$8 - } - $3 = $3 + 1 | 0; - if ($3 >>> 0 <= $11 >>> 0) { - continue - } - break; - }; - break label$8; - } - $7 = $7 + $6 | 0; - $4 = HEAP32[$0 + 4 >> 2]; - } - $3 = HEAP32[$4 + 7052 >> 2] + $6 | 0; - HEAP32[$4 + 7052 >> 2] = $3; - if ($3 >>> 0 > $11 >>> 0) { - $4 = 0; - if (!process_frame_($0, 0, 0)) { - break label$1 - } - if ($10) { - $4 = HEAP32[$0 + 4 >> 2]; - $3 = 0; - while (1) { - $6 = HEAP32[($4 + ($3 << 2) | 0) + 4 >> 2]; - HEAP32[$6 >> 2] = HEAP32[$6 + $13 >> 2]; - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - $4 = HEAP32[$0 + 4 >> 2]; - $5 = HEAP32[$0 >> 2]; - if (HEAP32[$5 + 16 >> 2]) { - $3 = HEAP32[$4 + 36 >> 2]; - HEAP32[$3 >> 2] = HEAP32[$3 + $13 >> 2]; - $3 = HEAP32[$4 + 40 >> 2]; - HEAP32[$3 >> 2] = HEAP32[$3 + $13 >> 2]; - } - HEAP32[$4 + 7052 >> 2] = 1; - } - if ($7 >>> 0 < $2 >>> 0) { - continue - } - break; - }; - $4 = 1; - } - return $4 | 0; - } - - function FLAC__stream_encoder_process_interleaved($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0; - $3 = HEAP32[$0 >> 2]; - $9 = HEAP32[$3 + 36 >> 2]; - $16 = $9 + 1 | 0; - label$1 : { - label$2 : { - $10 = HEAP32[$3 + 24 >> 2]; - if (!(!HEAP32[$3 + 16 >> 2] | ($10 | 0) != 2)) { - while (1) { - $4 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$3 + 4 >> 2]) { - $3 = HEAP32[$4 + 11800 >> 2]; - $5 = $16 - HEAP32[$4 + 7052 >> 2] | 0; - $6 = $2 - $7 | 0; - $8 = $5 >>> 0 < $6 >>> 0 ? $5 : $6; - label$6 : { - if (!$8) { - break label$6 - } - if (!$10) { - $3 = $3 + $8 | 0; - break label$6; - } - $5 = $7 << 1; - $11 = HEAP32[$4 + 11768 >> 2]; - $15 = HEAP32[$4 + 11764 >> 2]; - $6 = 0; - while (1) { - $13 = $3 << 2; - $14 = $5 << 2; - HEAP32[$13 + $15 >> 2] = HEAP32[$14 + $1 >> 2]; - HEAP32[$11 + $13 >> 2] = HEAP32[($14 | 4) + $1 >> 2]; - $3 = $3 + 1 | 0; - $5 = $5 + 2 | 0; - $6 = $6 + 1 | 0; - if (($8 | 0) != ($6 | 0)) { - continue - } - break; - }; - } - HEAP32[$4 + 11800 >> 2] = $3; - } - $5 = $7 >>> 0 < $2 >>> 0; - $3 = HEAP32[$4 + 7052 >> 2]; - label$9 : { - if ($3 >>> 0 > $9 >>> 0 | $7 >>> 0 >= $2 >>> 0) { - break label$9 - } - $11 = HEAP32[$4 + 40 >> 2]; - $15 = HEAP32[$4 + 8 >> 2]; - $13 = HEAP32[$4 + 36 >> 2]; - $14 = HEAP32[$4 + 4 >> 2]; - while (1) { - $5 = $3 << 2; - $8 = ($12 << 2) + $1 | 0; - $6 = HEAP32[$8 >> 2]; - HEAP32[$5 + $14 >> 2] = $6; - $8 = HEAP32[$8 + 4 >> 2]; - HEAP32[$5 + $15 >> 2] = $8; - HEAP32[$5 + $11 >> 2] = $6 - $8; - HEAP32[$5 + $13 >> 2] = $6 + $8 >> 1; - $3 = $3 + 1 | 0; - $12 = $12 + 2 | 0; - $7 = $7 + 1 | 0; - $5 = $7 >>> 0 < $2 >>> 0; - if ($7 >>> 0 >= $2 >>> 0) { - break label$9 - } - if ($3 >>> 0 <= $9 >>> 0) { - continue - } - break; - }; - } - HEAP32[$4 + 7052 >> 2] = $3; - if ($3 >>> 0 > $9 >>> 0) { - $3 = 0; - if (!process_frame_($0, 0, 0)) { - break label$1 - } - $3 = HEAP32[$0 + 4 >> 2]; - $6 = HEAP32[$3 + 4 >> 2]; - $4 = $6; - $6 = $9 << 2; - HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; - $4 = HEAP32[$3 + 8 >> 2]; - HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; - $4 = HEAP32[$3 + 36 >> 2]; - HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; - $4 = HEAP32[$3 + 40 >> 2]; - HEAP32[$4 >> 2] = HEAP32[$4 + $6 >> 2]; - HEAP32[$3 + 7052 >> 2] = 1; - } - if (!$5) { - break label$2 - } - $3 = HEAP32[$0 >> 2]; - continue; - } - } - while (1) { - $7 = HEAP32[$0 + 4 >> 2]; - if (HEAP32[$3 + 4 >> 2]) { - $6 = HEAP32[$7 + 11800 >> 2]; - $3 = $16 - HEAP32[$7 + 7052 >> 2] | 0; - $5 = $2 - $4 | 0; - $8 = $3 >>> 0 < $5 >>> 0 ? $3 : $5; - label$14 : { - if (!$8) { - break label$14 - } - if (!$10) { - $6 = $6 + $8 | 0; - break label$14; - } - $5 = Math_imul($4, $10); - $11 = 0; - while (1) { - $3 = 0; - while (1) { - HEAP32[HEAP32[($7 + ($3 << 2) | 0) + 11764 >> 2] + ($6 << 2) >> 2] = HEAP32[($5 << 2) + $1 >> 2]; - $5 = $5 + 1 | 0; - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - $6 = $6 + 1 | 0; - $11 = $11 + 1 | 0; - if (($8 | 0) != ($11 | 0)) { - continue - } - break; - }; - } - HEAP32[$7 + 11800 >> 2] = $6; - } - $6 = $4 >>> 0 < $2 >>> 0; - $5 = HEAP32[$7 + 7052 >> 2]; - label$18 : { - if ($5 >>> 0 > $9 >>> 0 | $4 >>> 0 >= $2 >>> 0) { - break label$18 - } - if ($10) { - while (1) { - $3 = 0; - while (1) { - HEAP32[HEAP32[($7 + ($3 << 2) | 0) + 4 >> 2] + ($5 << 2) >> 2] = HEAP32[($12 << 2) + $1 >> 2]; - $12 = $12 + 1 | 0; - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - $5 = $5 + 1 | 0; - $4 = $4 + 1 | 0; - $6 = $4 >>> 0 < $2 >>> 0; - if ($4 >>> 0 >= $2 >>> 0) { - break label$18 - } - if ($5 >>> 0 <= $9 >>> 0) { - continue - } - break label$18; - } - } - while (1) { - $5 = $5 + 1 | 0; - $4 = $4 + 1 | 0; - $6 = $4 >>> 0 < $2 >>> 0; - if ($4 >>> 0 >= $2 >>> 0) { - break label$18 - } - if ($5 >>> 0 <= $9 >>> 0) { - continue - } - break; - }; - } - HEAP32[$7 + 7052 >> 2] = $5; - if ($5 >>> 0 > $9 >>> 0) { - $3 = 0; - if (!process_frame_($0, 0, 0)) { - break label$1 - } - $5 = HEAP32[$0 + 4 >> 2]; - if ($10) { - $3 = 0; - while (1) { - $7 = HEAP32[($5 + ($3 << 2) | 0) + 4 >> 2]; - HEAP32[$7 >> 2] = HEAP32[$7 + ($9 << 2) >> 2]; - $3 = $3 + 1 | 0; - if (($10 | 0) != ($3 | 0)) { - continue - } - break; - }; - } - HEAP32[$5 + 7052 >> 2] = 1; - } - if (!$6) { - break label$2 - } - $3 = HEAP32[$0 >> 2]; - continue; - }; - } - $3 = 1; - } - return $3 | 0; - } - - function find_best_partition_order_($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) { - var $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0, $29 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $40 = 0; - $26 = $4 + $5 | 0; - $14 = FLAC__format_get_max_rice_partition_order_from_blocksize_limited_max_and_predictor_order($9, $26, $5); - $22 = $14 >>> 0 > $8 >>> 0 ? $8 : $14; - FUNCTION_TABLE[HEAP32[$0 + 7220 >> 2]]($1, $2, $4, $5, $22, $14, $10); - label$1 : { - if (!$11) { - break label$1 - } - $10 = 0; - $8 = 0; - if (($14 | 0) >= 0) { - $8 = 1 << $14; - $20 = $8 >>> 0 > 1 ? $8 : 1; - $16 = $26 >>> $14 | 0; - while (1) { - $17 = 0; - $9 = $13; - $18 = 0; - $27 = ($15 << 2) + $3 | 0; - label$4 : { - label$5 : { - $23 = $15 ? 0 : $5; - $19 = $16 - $23 | 0; - if (!$19) { - break label$5 - } - while (1) { - $21 = $17; - $17 = HEAP32[($9 << 2) + $1 >> 2]; - $17 = $21 | $17 >> 31 ^ $17; - $9 = $9 + 1 | 0; - $18 = $18 + 1 | 0; - if (($19 | 0) != ($18 | 0)) { - continue - } - break; - }; - $13 = ($13 + $16 | 0) - $23 | 0; - if (!$17) { - break label$5 - } - $9 = (Math_clz32($17) ^ 31) + 2 | 0; - break label$4; - } - $9 = 1; - } - HEAP32[$27 >> 2] = $9; - $15 = $15 + 1 | 0; - if (($20 | 0) != ($15 | 0)) { - continue - } - break; - }; - } - if (($14 | 0) <= ($22 | 0)) { - break label$1 - } - $1 = $14; - while (1) { - $1 = $1 + -1 | 0; - $9 = 0; - while (1) { - $13 = ($10 << 2) + $3 | 0; - $15 = HEAP32[$13 >> 2]; - $13 = HEAP32[$13 + 4 >> 2]; - HEAP32[($8 << 2) + $3 >> 2] = $15 >>> 0 > $13 >>> 0 ? $15 : $13; - $8 = $8 + 1 | 0; - $10 = $10 + 2 | 0; - $9 = $9 + 1 | 0; - if (!($9 >>> $1)) { - continue - } - break; - }; - if (($1 | 0) > ($22 | 0)) { - continue - } - break; - }; - } - label$9 : { - if (($14 | 0) < ($22 | 0)) { - HEAP32[$12 + 4 >> 2] = 0; - $2 = 6; - break label$9; - } - $28 = HEAP32[1407]; - $40 = $28 + (Math_imul($6 + 1 | 0, $4) - ($4 >>> 1 | 0) | 0) | 0; - $35 = $7 + -1 | 0; - $36 = HEAP32[1409] + HEAP32[1408] | 0; - $23 = HEAP32[1406] + HEAP32[1405] | 0; - $27 = $6 + -1 | 0; - while (1) { - label$12 : { - $20 = $14; - $37 = !$29; - $1 = Math_imul($37, 12) + $0 | 0; - $8 = $1 + 11724 | 0; - FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($8, $14 >>> 0 > 6 ? $14 : 6); - $38 = ($30 << 2) + $3 | 0; - $25 = ($30 << 3) + $2 | 0; - $39 = HEAP32[$1 + 11728 >> 2]; - $31 = HEAP32[$8 >> 2]; - label$13 : { - if ($14) { - $32 = $26 >>> $20 | 0; - if ($32 >>> 0 <= $5 >>> 0) { - break label$12 - } - $18 = 0; - $33 = 0; - $21 = $23; - if (!$11) { - while (1) { - $17 = $32 - ($18 ? 0 : $5) | 0; - $1 = $25 + ($18 << 3) | 0; - $13 = HEAP32[$1 + 4 >> 2]; - $16 = HEAP32[$1 >> 2]; - label$17 : { - if (!$13 & $16 >>> 0 >= 268435457 | $13 >>> 0 > 0) { - $1 = $17; - $10 = 0; - $8 = 0; - label$19 : { - if (($13 | 0) == 16777216 & $16 >>> 0 > 0 | $13 >>> 0 > 16777216) { - $14 = $1; - $9 = 0; - break label$19; - } - $14 = $1; - $9 = 0; - $15 = $1 >>> 25 | 0; - $19 = $1 << 7; - if (($13 | 0) == ($15 | 0) & $19 >>> 0 >= $16 >>> 0 | $15 >>> 0 > $13 >>> 0) { - break label$19 - } - while (1) { - $8 = $8 + 8 | 0; - $15 = $10 << 15 | $1 >>> 17; - $19 = $1 << 15; - $9 = $10 << 8 | $1 >>> 24; - $14 = $1 << 8; - $1 = $14; - $10 = $9; - if (($13 | 0) == ($15 | 0) & $19 >>> 0 < $16 >>> 0 | $15 >>> 0 < $13 >>> 0) { - continue - } - break; - }; - } - if (($9 | 0) == ($13 | 0) & $14 >>> 0 >= $16 >>> 0 | $9 >>> 0 > $13 >>> 0) { - break label$17 - } - while (1) { - $8 = $8 + 1 | 0; - $1 = $14; - $15 = $9 << 1 | $1 >>> 31; - $14 = $1 << 1; - $1 = $14; - $9 = $15; - if (($13 | 0) == ($9 | 0) & $1 >>> 0 < $16 >>> 0 | $9 >>> 0 < $13 >>> 0) { - continue - } - break; - }; - break label$17; - } - $8 = 0; - $10 = $17; - $1 = $16; - if ($10 << 3 >>> 0 < $1 >>> 0) { - while (1) { - $8 = $8 + 4 | 0; - $9 = $10 << 7; - $10 = $10 << 4; - if ($9 >>> 0 < $1 >>> 0) { - continue - } - break; - } - } - if ($10 >>> 0 >= $1 >>> 0) { - break label$17 - } - while (1) { - $8 = $8 + 1 | 0; - $10 = $10 << 1; - if ($10 >>> 0 < $1 >>> 0) { - continue - } - break; - }; - } - $8 = $8 >>> 0 < $7 >>> 0 ? $8 : $35; - $10 = $8 + -1 | 0; - $1 = $10 & 31; - $1 = (($28 - ($17 >>> 1 | 0) | 0) + Math_imul($17, $8 + 1 | 0) | 0) + ($8 ? (32 <= ($10 & 63) >>> 0 ? $13 >>> $1 | 0 : ((1 << $1) - 1 & $13) << 32 - $1 | $16 >>> $1) : $16 << 1) | 0; - $33 = ($1 | 0) == -1 ? $33 : $8; - HEAP32[$31 + ($18 << 2) >> 2] = $33; - $21 = $1 + $21 | 0; - $18 = $18 + 1 | 0; - if (!($18 >>> $20)) { - continue - } - break label$13; - } - } - while (1) { - $17 = $32 - ($18 ? 0 : $5) | 0; - $1 = $25 + ($18 << 3) | 0; - $13 = HEAP32[$1 + 4 >> 2]; - $16 = HEAP32[$1 >> 2]; - label$27 : { - label$28 : { - if (!$13 & $16 >>> 0 >= 268435457 | $13 >>> 0 > 0) { - $1 = $17; - $10 = 0; - $8 = 0; - if (($13 | 0) == 16777216 & $16 >>> 0 > 0 | $13 >>> 0 > 16777216) { - break label$28 - } - $14 = $1; - $9 = 0; - $15 = $1 >>> 25 | 0; - $19 = $1 << 7; - if (($13 | 0) == ($15 | 0) & $19 >>> 0 >= $16 >>> 0 | $15 >>> 0 > $13 >>> 0) { - break label$28 - } - while (1) { - $8 = $8 + 8 | 0; - $1 = $9; - $10 = $14; - $15 = $1 << 15 | $10 >>> 17; - $19 = $10 << 15; - $9 = $1 << 8; - $1 = $10; - $9 = $9 | $1 >>> 24; - $1 = $1 << 8; - $14 = $1; - $10 = $9; - if (($13 | 0) == ($15 | 0) & $19 >>> 0 < $16 >>> 0 | $15 >>> 0 < $13 >>> 0) { - continue - } - break; - }; - break label$28; - } - $8 = 0; - $10 = $17; - $1 = $16; - if ($10 << 3 >>> 0 < $1 >>> 0) { - while (1) { - $8 = $8 + 4 | 0; - $9 = $10 << 7; - $10 = $10 << 4; - if ($9 >>> 0 < $1 >>> 0) { - continue - } - break; - } - } - if ($10 >>> 0 >= $1 >>> 0) { - break label$27 - } - while (1) { - $8 = $8 + 1 | 0; - $10 = $10 << 1; - if ($10 >>> 0 < $1 >>> 0) { - continue - } - break; - }; - break label$27; - } - if (($10 | 0) == ($13 | 0) & $1 >>> 0 >= $16 >>> 0 | $10 >>> 0 > $13 >>> 0) { - break label$27 - } - while (1) { - $8 = $8 + 1 | 0; - $15 = $10 << 1 | $1 >>> 31; - $1 = $1 << 1; - $10 = $15; - if (($13 | 0) == ($10 | 0) & $1 >>> 0 < $16 >>> 0 | $10 >>> 0 < $13 >>> 0) { - continue - } - break; - }; - } - $9 = $18 << 2; - $1 = HEAP32[$9 + $38 >> 2]; - $19 = $1; - $10 = Math_imul($1, $17) + $36 | 0; - $8 = $8 >>> 0 < $7 >>> 0 ? $8 : $35; - $15 = $8 + -1 | 0; - $1 = $15 & 31; - $14 = (($28 - ($17 >>> 1 | 0) | 0) + Math_imul($17, $8 + 1 | 0) | 0) + ($8 ? (32 <= ($15 & 63) >>> 0 ? $13 >>> $1 | 0 : ((1 << $1) - 1 & $13) << 32 - $1 | $16 >>> $1) : $16 << 1) | 0; - $1 = $10 >>> 0 > $14 >>> 0; - HEAP32[$9 + $39 >> 2] = $1 ? 0 : $19; - HEAP32[$9 + $31 >> 2] = $1 ? $8 : 0; - $21 = ($1 ? $14 : $10) + $21 | 0; - $18 = $18 + 1 | 0; - if (!($18 >>> $20)) { - continue - } - break; - }; - break label$13; - } - $9 = HEAP32[$25 + 4 >> 2]; - $1 = $27; - $8 = $1 & 31; - $10 = HEAP32[$25 >> 2]; - $8 = ($6 ? (32 <= ($1 & 63) >>> 0 ? $9 >>> $8 | 0 : ((1 << $8) - 1 & $9) << 32 - $8 | $10 >>> $8) : $10 << 1) + $40 | 0; - $10 = ($8 | 0) == -1 ? 0 : $6; - if ($11) { - $9 = HEAP32[$38 >> 2]; - $14 = Math_imul($9, $4) + $36 | 0; - $1 = $14 >>> 0 > $8 >>> 0; - HEAP32[$39 >> 2] = $1 ? 0 : $9; - $10 = $1 ? $10 : 0; - $8 = $1 ? $8 : $14; - } - HEAP32[$31 >> 2] = $10; - $21 = $8 + $23 | 0; - } - $1 = $34 + -1 >>> 0 < $21 >>> 0; - $24 = $1 ? $24 : $20; - $29 = $1 ? $29 : $37; - $34 = $1 ? $34 : $21; - $14 = $20 + -1 | 0; - $30 = (1 << $20) + $30 | 0; - if (($20 | 0) > ($22 | 0)) { - continue - } - } - break; - }; - HEAP32[$12 + 4 >> 2] = $24; - $2 = $24 >>> 0 > 6 ? $24 : 6; - } - $1 = HEAP32[$12 + 8 >> 2]; - FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size($1, $2); - $2 = Math_imul($29, 12) + $0 | 0; - $0 = 1 << $24; - $3 = $0 << 2; - memcpy(HEAP32[$1 >> 2], HEAP32[$2 + 11724 >> 2], $3); - if ($11) { - memcpy(HEAP32[$1 + 4 >> 2], HEAP32[$2 + 11728 >> 2], $3) - } - $0 = $0 >>> 0 > 1 ? $0 : 1; - $2 = HEAP32[1410]; - $1 = HEAP32[$1 >> 2]; - $8 = 0; - label$37 : { - while (1) { - if (HEAPU32[$1 + ($8 << 2) >> 2] < $2 >>> 0) { - $8 = $8 + 1 | 0; - if (($0 | 0) != ($8 | 0)) { - continue - } - break label$37; - } - break; - }; - HEAP32[$12 >> 2] = 1; - } - return $34; - } - - function stackSave() { - return global$0 | 0; - } - - function stackRestore($0) { - $0 = $0 | 0; - global$0 = $0; - } - - function stackAlloc($0) { - $0 = $0 | 0; - $0 = global$0 - $0 & -16; - global$0 = $0; - return $0 | 0; - } - - function __growWasmMemory($0) { - $0 = $0 | 0; - return __wasm_memory_grow($0 | 0) | 0; - } - - function dynCall_iii($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - return FUNCTION_TABLE[$0]($1, $2) | 0; - } - - function dynCall_ii($0, $1) { - $0 = $0 | 0; - $1 = $1 | 0; - return FUNCTION_TABLE[$0]($1) | 0; - } - - function dynCall_iiii($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - return FUNCTION_TABLE[$0]($1, $2, $3) | 0; - } - - function dynCall_viiiiii($0, $1, $2, $3, $4, $5, $6) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - FUNCTION_TABLE[$0]($1, $2, $3, $4, $5, $6); - } - - function dynCall_iiiii($0, $1, $2, $3, $4) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - return FUNCTION_TABLE[$0]($1, $2, $3, $4) | 0; - } - - function dynCall_viiiiiii($0, $1, $2, $3, $4, $5, $6, $7) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $5 = $5 | 0; - $6 = $6 | 0; - $7 = $7 | 0; - FUNCTION_TABLE[$0]($1, $2, $3, $4, $5, $6, $7); - } - - function dynCall_viiii($0, $1, $2, $3, $4) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - FUNCTION_TABLE[$0]($1, $2, $3, $4); - } - - function dynCall_viii($0, $1, $2, $3) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - FUNCTION_TABLE[$0]($1, $2, $3); - } - - function legalstub$FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - return FLAC__stream_encoder_set_total_samples_estimate($0, $1, $2) | 0; - } - - function legalstub$dynCall_jiji($0, $1, $2, $3, $4) { - $0 = $0 | 0; - $1 = $1 | 0; - $2 = $2 | 0; - $3 = $3 | 0; - $4 = $4 | 0; - $0 = FUNCTION_TABLE[$0]($1, $2, $3, $4) | 0; - setTempRet0(i64toi32_i32$HIGH_BITS | 0); - return $0 | 0; - } - - function _ZN17compiler_builtins3int3mul3Mul3mul17h070e9a1c69faec5bE($0, $1, $2, $3) { - var $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0; - $4 = $2 >>> 16 | 0; - $5 = $0 >>> 16 | 0; - $9 = Math_imul($4, $5); - $6 = $2 & 65535; - $7 = $0 & 65535; - $8 = Math_imul($6, $7); - $5 = ($8 >>> 16 | 0) + Math_imul($5, $6) | 0; - $4 = ($5 & 65535) + Math_imul($4, $7) | 0; - $0 = (Math_imul($1, $2) + $9 | 0) + Math_imul($0, $3) + ($5 >>> 16) + ($4 >>> 16) | 0; - $1 = $8 & 65535 | $4 << 16; - i64toi32_i32$HIGH_BITS = $0; - return $1; - } - - function _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, $2) { - var $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $10 = 0, $11 = 0; - label$1 : { - label$2 : { - label$3 : { - label$4 : { - label$5 : { - label$6 : { - label$7 : { - label$9 : { - label$11 : { - $3 = $1; - if ($3) { - $4 = $2; - if (!$4) { - break label$11 - } - break label$9; - } - $1 = $0; - $0 = ($0 >>> 0) / ($2 >>> 0) | 0; - __wasm_intrinsics_temp_i64 = $1 - Math_imul($0, $2) | 0; - __wasm_intrinsics_temp_i64$hi = 0; - i64toi32_i32$HIGH_BITS = 0; - return $0; - } - if (!$0) { - break label$7 - } - break label$6; - } - $6 = $4 + -1 | 0; - if (!($6 & $4)) { - break label$5 - } - $6 = (Math_clz32($4) + 33 | 0) - Math_clz32($3) | 0; - $7 = 0 - $6 | 0; - break label$3; - } - __wasm_intrinsics_temp_i64 = 0; - $0 = ($3 >>> 0) / 0 | 0; - __wasm_intrinsics_temp_i64$hi = $3 - Math_imul($0, 0) | 0; - i64toi32_i32$HIGH_BITS = 0; - return $0; - } - $3 = 32 - Math_clz32($3) | 0; - if ($3 >>> 0 < 31) { - break label$4 - } - break label$2; - } - __wasm_intrinsics_temp_i64 = $0 & $6; - __wasm_intrinsics_temp_i64$hi = 0; - if (($4 | 0) == 1) { - break label$1 - } - $3 = __wasm_ctz_i32($4); - $2 = $3 & 31; - if (32 <= ($3 & 63) >>> 0) { - $4 = 0; - $0 = $1 >>> $2 | 0; - } else { - $4 = $1 >>> $2 | 0; - $0 = ((1 << $2) - 1 & $1) << 32 - $2 | $0 >>> $2; - } - i64toi32_i32$HIGH_BITS = $4; - return $0; - } - $6 = $3 + 1 | 0; - $7 = 63 - $3 | 0; - } - $3 = $1; - $4 = $6 & 63; - $5 = $4 & 31; - if (32 <= $4 >>> 0) { - $4 = 0; - $5 = $3 >>> $5 | 0; - } else { - $4 = $3 >>> $5 | 0; - $5 = ((1 << $5) - 1 & $3) << 32 - $5 | $0 >>> $5; - } - $7 = $7 & 63; - $3 = $7 & 31; - if (32 <= $7 >>> 0) { - $1 = $0 << $3; - $0 = 0; - } else { - $1 = (1 << $3) - 1 & $0 >>> 32 - $3 | $1 << $3; - $0 = $0 << $3; - } - if ($6) { - $7 = -1; - $3 = $2 + -1 | 0; - if (($3 | 0) != -1) { - $7 = 0 - } - while (1) { - $8 = $5 << 1 | $1 >>> 31; - $9 = $8; - $4 = $4 << 1 | $5 >>> 31; - $8 = $7 - ($4 + ($3 >>> 0 < $8 >>> 0) | 0) >> 31; - $10 = $2 & $8; - $5 = $9 - $10 | 0; - $4 = $4 - ($9 >>> 0 < $10 >>> 0) | 0; - $1 = $1 << 1 | $0 >>> 31; - $0 = $11 | $0 << 1; - $8 = $8 & 1; - $11 = $8; - $6 = $6 + -1 | 0; - if ($6) { - continue - } - break; - }; - } - __wasm_intrinsics_temp_i64 = $5; - __wasm_intrinsics_temp_i64$hi = $4; - i64toi32_i32$HIGH_BITS = $1 << 1 | $0 >>> 31; - return $8 | $0 << 1; - } - __wasm_intrinsics_temp_i64 = $0; - __wasm_intrinsics_temp_i64$hi = $1; - $0 = 0; - $1 = 0; - } - i64toi32_i32$HIGH_BITS = $1; - return $0; - } - - function __wasm_ctz_i32($0) { - if ($0) { - return 31 - Math_clz32($0 + -1 ^ $0) | 0 - } - return 32; - } - - function __wasm_i64_mul($0, $1, $2, $3) { - $0 = _ZN17compiler_builtins3int3mul3Mul3mul17h070e9a1c69faec5bE($0, $1, $2, $3); - return $0; - } - - function __wasm_i64_udiv($0, $1, $2) { - return _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, $2); - } - - function __wasm_i64_urem($0, $1) { - _ZN17compiler_builtins3int4udiv10divmod_u6417h6026910b5ed08e40E($0, $1, 588); - i64toi32_i32$HIGH_BITS = __wasm_intrinsics_temp_i64$hi; - return __wasm_intrinsics_temp_i64; - } - - function __wasm_rotl_i32($0, $1) { - var $2 = 0, $3 = 0; - $2 = $1 & 31; - $3 = (-1 >>> $2 & $0) << $2; - $2 = $0; - $0 = 0 - $1 & 31; - return $3 | ($2 & -1 << $0) >>> $0; - } - - // EMSCRIPTEN_END_FUNCS -; - FUNCTION_TABLE[1] = seekpoint_compare_; - FUNCTION_TABLE[2] = __stdio_close; - FUNCTION_TABLE[3] = __stdio_read; - FUNCTION_TABLE[4] = __stdio_seek; - FUNCTION_TABLE[5] = FLAC__lpc_restore_signal; - FUNCTION_TABLE[6] = FLAC__lpc_restore_signal_wide; - FUNCTION_TABLE[7] = read_callback_; - FUNCTION_TABLE[8] = read_callback_proxy_; - FUNCTION_TABLE[9] = __emscripten_stdout_close; - FUNCTION_TABLE[10] = __stdio_write; - FUNCTION_TABLE[11] = __emscripten_stdout_seek; - FUNCTION_TABLE[12] = FLAC__lpc_compute_residual_from_qlp_coefficients; - FUNCTION_TABLE[13] = FLAC__lpc_compute_residual_from_qlp_coefficients_wide; - FUNCTION_TABLE[14] = FLAC__fixed_compute_best_predictor_wide; - FUNCTION_TABLE[15] = FLAC__fixed_compute_best_predictor; - FUNCTION_TABLE[16] = precompute_partition_info_sums_; - FUNCTION_TABLE[17] = FLAC__lpc_compute_autocorrelation; - FUNCTION_TABLE[18] = verify_read_callback_; - FUNCTION_TABLE[19] = verify_write_callback_; - FUNCTION_TABLE[20] = verify_metadata_callback_; - FUNCTION_TABLE[21] = verify_error_callback_; - function __wasm_memory_size() { - return buffer.byteLength / 65536 | 0; - } - - function __wasm_memory_grow(pagesToAdd) { - pagesToAdd = pagesToAdd | 0; - var oldPages = __wasm_memory_size() | 0; - var newPages = oldPages + pagesToAdd | 0; - if ((oldPages < newPages) && (newPages < 65536)) { - var newBuffer = new ArrayBuffer(Math_imul(newPages, 65536)); - var newHEAP8 = new global.Int8Array(newBuffer); - newHEAP8.set(HEAP8); - HEAP8 = newHEAP8; - HEAP8 = new global.Int8Array(newBuffer); - HEAP16 = new global.Int16Array(newBuffer); - HEAP32 = new global.Int32Array(newBuffer); - HEAPU8 = new global.Uint8Array(newBuffer); - HEAPU16 = new global.Uint16Array(newBuffer); - HEAPU32 = new global.Uint32Array(newBuffer); - HEAPF32 = new global.Float32Array(newBuffer); - HEAPF64 = new global.Float64Array(newBuffer); - buffer = newBuffer; - memory.buffer = newBuffer; - } - return oldPages; - } - - return { - "__wasm_call_ctors": __wasm_call_ctors, - "FLAC__stream_decoder_new": FLAC__stream_decoder_new, - "FLAC__stream_decoder_delete": FLAC__stream_decoder_delete, - "FLAC__stream_decoder_finish": FLAC__stream_decoder_finish, - "FLAC__stream_decoder_init_stream": FLAC__stream_decoder_init_stream, - "FLAC__stream_decoder_reset": FLAC__stream_decoder_reset, - "FLAC__stream_decoder_init_ogg_stream": FLAC__stream_decoder_init_ogg_stream, - "FLAC__stream_decoder_set_ogg_serial_number": FLAC__stream_decoder_set_ogg_serial_number, - "FLAC__stream_decoder_set_md5_checking": FLAC__stream_decoder_set_md5_checking, - "FLAC__stream_decoder_set_metadata_respond": FLAC__stream_decoder_set_metadata_respond, - "FLAC__stream_decoder_set_metadata_respond_application": FLAC__stream_decoder_set_metadata_respond_application, - "FLAC__stream_decoder_set_metadata_respond_all": FLAC__stream_decoder_set_metadata_respond_all, - "FLAC__stream_decoder_set_metadata_ignore": FLAC__stream_decoder_set_metadata_ignore, - "FLAC__stream_decoder_set_metadata_ignore_application": FLAC__stream_decoder_set_metadata_ignore_application, - "FLAC__stream_decoder_set_metadata_ignore_all": FLAC__stream_decoder_set_metadata_ignore_all, - "FLAC__stream_decoder_get_state": FLAC__stream_decoder_get_state, - "FLAC__stream_decoder_get_md5_checking": FLAC__stream_decoder_get_md5_checking, - "FLAC__stream_decoder_process_single": FLAC__stream_decoder_process_single, - "FLAC__stream_decoder_process_until_end_of_metadata": FLAC__stream_decoder_process_until_end_of_metadata, - "FLAC__stream_decoder_process_until_end_of_stream": FLAC__stream_decoder_process_until_end_of_stream, - "FLAC__stream_encoder_new": FLAC__stream_encoder_new, - "FLAC__stream_encoder_delete": FLAC__stream_encoder_delete, - "FLAC__stream_encoder_finish": FLAC__stream_encoder_finish, - "FLAC__stream_encoder_init_stream": FLAC__stream_encoder_init_stream, - "FLAC__stream_encoder_init_ogg_stream": FLAC__stream_encoder_init_ogg_stream, - "FLAC__stream_encoder_set_ogg_serial_number": FLAC__stream_encoder_set_ogg_serial_number, - "FLAC__stream_encoder_set_verify": FLAC__stream_encoder_set_verify, - "FLAC__stream_encoder_set_channels": FLAC__stream_encoder_set_channels, - "FLAC__stream_encoder_set_bits_per_sample": FLAC__stream_encoder_set_bits_per_sample, - "FLAC__stream_encoder_set_sample_rate": FLAC__stream_encoder_set_sample_rate, - "FLAC__stream_encoder_set_compression_level": FLAC__stream_encoder_set_compression_level, - "FLAC__stream_encoder_set_blocksize": FLAC__stream_encoder_set_blocksize, - "FLAC__stream_encoder_set_total_samples_estimate": legalstub$FLAC__stream_encoder_set_total_samples_estimate, - "FLAC__stream_encoder_set_metadata": FLAC__stream_encoder_set_metadata, - "FLAC__stream_encoder_get_state": FLAC__stream_decoder_get_state, - "FLAC__stream_encoder_get_verify_decoder_state": FLAC__stream_encoder_get_verify_decoder_state, - "FLAC__stream_encoder_get_verify": FLAC__stream_encoder_get_verify, - "FLAC__stream_encoder_process": FLAC__stream_encoder_process, - "FLAC__stream_encoder_process_interleaved": FLAC__stream_encoder_process_interleaved, - "__errno_location": __errno_location, - "stackSave": stackSave, - "stackRestore": stackRestore, - "stackAlloc": stackAlloc, - "malloc": dlmalloc, - "free": dlfree, - "__growWasmMemory": __growWasmMemory, - "dynCall_iii": dynCall_iii, - "dynCall_ii": dynCall_ii, - "dynCall_iiii": dynCall_iiii, - "dynCall_jiji": legalstub$dynCall_jiji, - "dynCall_viiiiii": dynCall_viiiiii, - "dynCall_iiiii": dynCall_iiiii, - "dynCall_viiiiiii": dynCall_viiiiiii, - "dynCall_viiii": dynCall_viiii, - "dynCall_viii": dynCall_viii - }; -} - -var bufferView = new Uint8Array(wasmMemory.buffer); -for (var base64ReverseLookup = new Uint8Array(123/*'z'+1*/), i = 25; i >= 0; --i) { - base64ReverseLookup[48+i] = 52+i; // '0-9' - base64ReverseLookup[65+i] = i; // 'A-Z' - base64ReverseLookup[97+i] = 26+i; // 'a-z' - } - base64ReverseLookup[43] = 62; // '+' - base64ReverseLookup[47] = 63; // '/' - /** @noinline Inlining this function would mean expanding the base64 string 4x times in the source code, which Closure seems to be happy to do. */ - function base64DecodeToExistingUint8Array(uint8Array, offset, b64) { - var b1, b2, i = 0, j = offset, bLength = b64.length, end = offset + (bLength*3>>2) - (b64[bLength-2] == '=') - (b64[bLength-1] == '='); - for (; i < bLength; i += 4) { - b1 = base64ReverseLookup[b64.charCodeAt(i+1)]; - b2 = base64ReverseLookup[b64.charCodeAt(i+2)]; - uint8Array[j++] = base64ReverseLookup[b64.charCodeAt(i)] << 2 | b1 >> 4; - if (j < end) uint8Array[j++] = b1 << 4 | b2 >> 2; - if (j < end) uint8Array[j++] = b2 << 6 | base64ReverseLookup[b64.charCodeAt(i+3)]; - } - } - base64DecodeToExistingUint8Array(bufferView, 1025, "Bw4JHBsSFTg/NjEkIyotcHd+eWxrYmVIT0ZBVFNaXeDn7un8+/L12N/W0cTDys2Ql56ZjIuChaivpqG0s7q9x8DJztvc1dL/+PH24+Tt6rewub6rrKWij4iBhpOUnZonICkuOzw1Mh8YERYDBA0KV1BZXktMRUJvaGFmc3R9eomOh4CVkpucsba/uK2qo6T5/vfw5eLr7MHGz8jd2tPUaW5nYHVye3xRVl9YTUpDRBkeFxAFAgsMISYvKD06MzROSUBHUlVcW3ZxeH9qbWRjPjkwNyIlLCsGAQgPGh0UE66poKeytby7lpGYn4qNhIPe2dDXwsXMy+bh6O/6/fTzAAAFgA+ACgAbgB4AFAARgDOANgA8ADmAKAAtgCeAIgBjgGYAbABpgHgAfYB3gHIAUABVgF+AWgBLgE4ARABBgMOAxgDMAMmA2ADdgNeA0gDwAPWA/4D6AOuA7gDkAOGAoAClgK+AqgC7gL4AtACxgJOAlgCcAJmAiACNgIeAggCDgYYBjAGJgZgBnYGXgZIBsAG1gb+BugGrga4BpAGhgeAB5YHvgeoB+4H+AfQB8YHTgdYB3AHZgcgBzYHHgcIBQAFFgU+BSgFbgV4BVAFRgXOBdgF8AXmBaAFtgWeBYgEjgSYBLAEpgTgBPYE3gTIBEAEVgR+BGgELgQ4BBAEBgQODBgMMAwmDGAMdgxeDEgMwAzWDP4M6AyuDLgMkAyGDYANlg2+DagN7g34DdANxg1ODVgNcA1mDSANNg0eDQgPAA8WDz4PKA9uD3gPUA9GD84P2A/wD+YPoA+2D54PiA6ODpgOsA6mDuAO9g7eDsgOQA5WDn4OaA4uDjgOEA4GDgAKFgo+CigKbgp4ClAKRgrOCtgK8ArmCqAKtgqeCogLjguYC7ALpgvgC/YL3gvIC0ALVgt+C2gLLgs4CxALBgkOCRgJMAkmCWAJdgleCUgJwAnWCf4J6AmuCbgJkAmGCIAIlgi+CKgI7gj4CNAIxghOCFgIcAhmCCAINggeCAgIAAAOGA4wACgOYAB4AFAOSA7AANgA8A7oAKAOuA6QAIgPgAGYAbAPqAHgD/gP0AHIAUAPWA9wAWgPIAE4ARAPCA0AAxgDMA0oA2ANeA1QA0gDwA3YDfAD6A2gA7gDkA2IAoAMmAywAqgM4AL4AtAMyAxAAlgCcAxoAiAMOAwQAggaABQYFDAaKBRgGngaUBRIFMAa2BrwFOgaoBS4FJAaiBWAG5gbsBWoG+AV+BXQG8gbQBVYFXAbaBUgGzgbEBUIFwAZGBkwFygZYBd4F1AZSBnAF9gX8BnoF6AZuBmQF4gYgBaYFrAYqBbgGPgY0BbIFkAYWBhwFmgYIBY4FhAYCCYAKBgoMCYoKGAmeCZQKEgowCbYJvAo6CagKLgokCaIKYAnmCewKagn4Cn4KdAnyCdAKVgpcCdoKSAnOCcQKQgrACUYJTArKCVgK3grUCVIJcAr2CvwJegroCW4JZAriCSAKpgqsCSoKuAk+CTQKsgqQCRYJHAqaCQgKjgqECQIPAAyGDIwPCgyYDx4PFAySDLAPNg88DLoPKAyuDKQPIgzgD2YPbAzqD3gM/gz0D3IPUAzWDNwPWgzID04PRAzCDEAPxg/MDEoP2AxeDFQP0g/wDHYMfA/6DGgP7g/kDGIPoAwmDCwPqgw4D74PtAwyDBAPlg+cDBoPiAwODAQPggAAF4ArgDwAU4BEAHgAb4CjgLQAiACfgPAA54DbgMwAQ4FUAWgBf4EQAQeBO4EsAeAB94HLgdwBs4GkAZgBj4GDgpQCqAK/gtACx4L7guwCIAI3gguCHAJzgmQCWAJPgsAD14Prg/wDk4OEA7gDr4Njg3QDSANfgzADJ4MbgwwDA4UUBSgFP4VQBUeFe4VsBaAFt4WLhZwF84XkBdgFz4VABFeEa4R8BBOEBAQ4BC+E44T0BMgE34SwBKeEm4SMBIAHl4erh7wH04fEB/gH74cjhzQHCAcfh3AHZ4dbh0wHw4bUBugG/4aQBoeGu4asBmAGd4ZLhlwGM4YkBhgGD4YDihQKKAo/ilAKR4p7imwKoAq3iouKnArziuQK2ArPikALV4tri3wLE4sECzgLL4vji/QLyAvfi7ALp4ubi4wLgAiXiKuIvAjTiMQI+AjviCOINAgICB+IcAhniFuITAjDidQJ6An/iZAJh4m7iawJYAl3iUuJXAkziSQJGAkPiQAPF48rjzwPU49ED3gPb4+jj7QPiA+fj/AP54/bj8wPQ45UDmgOf44QDgeOO44sDuAO947LjtwOs46kDpgOj46DjZQNqA2/jdANx437jewNIA03jQuNHA1zjWQNWA1PjcAM14zrjPwMk4yEDLgMr4xjjHQMSAxfjDAMJ4wbjAwMAAADlAOoADwD0ABEAHgD7AMgALQAiAMcAPADZANYAMwGQAXUBegGfAWQBgQGOAWsBWAG9AbIBVwGsAUkBRgGjAyADxQPKAy8D1AMxAz4D2wPoAw0DAgPnAxwD+QP2AxMCsAJVAloCvwJEAqECrgJLAngCnQKSAncCjAJpAmYCgwdgB4UHigdvB5QHcQd+B5sHqAdNB0IHpwdcB7kHtgdTBvAGFQYaBv8GBAbhBu4GCwY4Bt0G0gY3BswGKQYmBsMEQASlBKoETwS0BFEEXgS7BIgEbQRiBIcEfASZBJYEcwXQBTUFOgXfBSQFwQXOBSsFGAX9BfIFFwXsBQkFBgXjD+APBQ8KD+8PFA/xD/4PGw8oD80Pwg8nD9wPOQ82D9MOcA6VDpoOfw6EDmEObg6LDrgOXQ5SDrcOTA6pDqYOQwzADCUMKgzPDDQM0QzeDDsMCAztDOIMBwz8DBkMFgzzDVANtQ26DV8NpA1BDU4Nqw2YDX0Ncg2XDWwNiQ2GDWMIgAhlCGoIjwh0CJEIngh7CEgIrQiiCEcIvAhZCFYIswkQCfUJ+gkfCeQJAQkOCesJ2Ak9CTIJ1wksCckJxgkjC6ALRQtKC68LVAuxC74LWwtoC40LggtnC5wLeQt2C5MKMArVCtoKPwrECiEKLgrLCvgKHQoSCvcKDArpCuYKAwAAHuA84CIAOOBmAEQAWuBw4O4AzADS4MgAluC04KoAoOH+AdwBwuHYAYbhpOG6AZABDuEs4TIBKOF2AVQBSuFA494D/APi4/gDpuOE45oDsAMu4wzjEgMI41YDdANq42ACPuIc4gICGOJGAmQCeuJQ4s4C7ALy4ugCtuKU4ooCgOeeB7wHoue4B+bnxOfaB/AHbudM51IHSOcWBzQHKucgBn7mXOZCBljmBgYkBjrmEOaOBqwGsuaoBvbm1ObKBsAEXuR85GIEeOQmBAQEGuQw5K4EjASS5IgE1uT05OoE4OW+BZwFguWYBcbl5OX6BdAFTuVs5XIFaOU2BRQFCuUA7x4PPA8i7zgPZu9E71oPcA/u78zv0g/I75YPtA+q76AO/u7c7sIO2O6GDqQOuu6Q7g4OLA4y7igOdu5U7koOQAze7Pzs4gz47KYMhAya7LDsLgwMDBLsCAxW7HTsagxg7T4NHA0C7RgNRu1k7XoNUA3O7ezt8g3o7bYNlA2K7YAInui86KIIuOjmCMQI2ujw6G4ITAhS6EgIFug06CoIIOl+CVwJQulYCQbpJOk6CRAJjums6bIJqOn2CdQJyunA614LfAti63gLJusE6xoLMAuu64zrkguI69YL9Avq6+AKvuqc6oIKmOrGCuQK+urQ6k4KbApy6mgKNuoU6goKAAAA/gDcACIBuAFGAWQBmgJQAq4CjAJyA+gDFgM0A8oEoAReBHwEggUYBeYFxAU6BvAGDgYsBtIHSAe2B5QHaghgCJ4IvAhCCdgJJgkECfoKMArOCuwKEguIC3YLVAuqDMAMPgwcDOINeA2GDaQNWg6QDm4OTA6yDygP1g/0DwoR4BEeETwRwhBYEKYQhBB6E7ATThNsE5ISCBL2EtQSKhVAFb4VnBViFPgUBhQkFNoXEBfuF8wXMhaoFlYWdBaKGYAZfhlcGaIYOBjGGOQYGhvQGy4bDBvyGmgalhq0GkodIB3eHfwdAhyYHGYcRBy6H3Afjh+sH1IeyB42HhQe6iLgIh4iPCLCI1gjpiOEI3ogsCBOIGwgkiEIIfYh1CEqJkAmviacJmIn+CcGJyQn2iQQJO4kzCQyJaglViV0JYoqgCp+Klwqois4K8Yr5CsaKNAoLigMKPIpaCmWKbQpSi4gLt4u/C4CL5gvZi9EL7oscCyOLKwsUi3ILTYtFC3qMwAz/jPcMyIyuDJGMmQymjFQMa4xjDFyMOgwFjA0MMo3oDdeN3w3gjYYNuY2xDY6NfA1DjUsNdI0SDS2NJQ0ajtgO547vDtCOtg6JjoEOvo5MDnOOew5EjiIOHY4VDiqP8A/Pj8cP+I+eD6GPqQ+Wj2QPW49TD2yPCg81jz0PAoAAATgSOCMANDhFAFYAZzh4OIkAmgCrOLwAzTjeOO8A8DkRAQIBMzkkAVU5Rjl3AWgBmTmKObsBrDndAc4B/zngOiECMgIDOhQCZTp2OkcCWAKpOro6iwKcOu0C/gLPOtADMTsiOxMDBDt1A2YDVztIO7kDqgObO4wD/TvuO98DwDxBBFIEYzx0BAU8FjwnBDgEyTzaPOsE/DyNBJ4ErzywBVE9Qj1zBWQ9FQUGBTc9KD3ZBcoF+z3sBZ09jj2/BaAGYT5yPkMGVD4lBjYGBz4YPukG+gbLPtwGrT6+Po8GkD9xB2IHUz9EBzU/Jj8XBwgH+T/qP9sHzD+9B64Hnz+AMIEIkgijMLQIxTDWMOcI+AgJMBowKwg8ME0IXghvMHAJkTGCMbMJpDHVCcYJ9zHoMRkJCgk7MSwJXTFOMX8JYAqhMrIygwqUMuUK9grHMtgyKQo6CgsyHAptMn4yTwpQM7ELoguTM4QL9TPmM9cLyAs5MyozGwsMM30LbgtfM0AMwTTSNOMM9DSFDJYMpzS4NEkMWgxrNHwMDTQeNC8MMDXRDcIN8zXkDZU1hjW3DagNWTVKNXsNbDUdDQ4NPzUgNuEO8g7DNtQOpTa2NocOmA5pNno2Sw5cNi0OPg4PNhAP8TfiN9MPxDe1D6YPlzeIN3kPag9bN0wPPTcuNx8PAAAAYQDCAKMBhAHlAUYBJwMIA2kDygOrAowC7QJOAi8GWAY5BpoG+wfcB70HHgd/BVAFMQWSBfME1AS1BBYEdwz4DJkMOgxbDXwNHQ2+Dd8P8A+RDzIPUw50DhUOtg7XCqAKwQpiCgMLJAtFC+YLhwmoCckJagkLCCwITQjuCI8JuBnZGXoZGxg8GF0Y/hifGrAa0RpyGhMbNBtVG/Yblx/gH4EfIh9DHmQeBR6mHscc6ByJHCocSx1sHQ0drh3PFUAVIRWCFeMUxBSlFAYUZxZIFikWihbrF8wXrRcOF28TGBN5E9oTuxKcEv0SXhI/EBAQcRDSELMRlBH1EVYRNxM4M1kz+jObMrwy3TJ+Mh8wMDBRMPIwkzG0MdUxdjEXNWA1ATWiNcM05DSFNCY0RzZoNgk2qjbLN+w3jTcuN08/wD+hPwI/Yz5EPiU+hj7nPMg8qTwKPGs9TD0tPY497zmYOfk5Wjk7OBw4fTjeOL86kDrxOlI6MzsUO3U71ju3OoAq4SpCKiMrBCtlK8YrpymIKekpSikrKAwobSjOKK8s2Cy5LBosey1cLT0tni3/L9AvsS8SL3MuVC41LpYu9yZ4JhkmuibbJ/wnnSc+J18lcCURJbIl0yT0JJUkNiRXICAgQSDiIIMhpCHFIWYhByMoI0kj6iOLIqwizSJuIg8nJlZmVyZW5jZSBsaWJGTEFDIDEuMy4zIDIwMTkwODA0AGZMYUMAAABDYUxmIAAAABAAAAAQAAAAGAAAABgAAAAUAAAAAwAAAAUAAAAkAAAAIAAAAEAAAABAAAAAEAAAAEAAAAAIAAAAGAAAAEAAAAAIAAAAYAAAAAEAAAABAAAAbgAAAAgAAAAABAAAQAAAAAEAAAAXCAAACAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAAQAAAAcAAAAYAAAA/j8AAA4AAAABAAAAAQAAAAQAAAAEAAAABAAAAAMAAAABAAAACAAAABAAAAACAAAABAAAAAQAAAAFAAAABQAAAA8AAAAfAAAABAAAAAUAAAABAAAABgAAAAEAAAAAAAAAAgAAABAAAABAAAAAQ0QtREEgY3VlIHNoZWV0IG11c3QgaGF2ZSBhIGxlYWQtaW4gbGVuZ3RoIG9mIGF0IGxlYXN0IDIgc2Vjb25kcwBDRC1EQSBjdWUgc2hlZXQgbGVhZC1pbiBsZW5ndGggbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAGN1ZSBzaGVldCBtdXN0IGhhdmUgYXQgbGVhc3Qgb25lIHRyYWNrICh0aGUgbGVhZC1vdXQpAENELURBIGN1ZSBzaGVldCBtdXN0IGhhdmUgYSBsZWFkLW91dCB0cmFjayBudW1iZXIgMTcwICgweEFBKQBjdWUgc2hlZXQgbWF5IG5vdCBoYXZlIGEgdHJhY2sgbnVtYmVyIDAAQ0QtREEgY3VlIHNoZWV0IHRyYWNrIG51bWJlciBtdXN0IGJlIDEtOTkgb3IgMTcwAENELURBIGN1ZSBzaGVldCBsZWFkLW91dCBvZmZzZXQgbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAENELURBIGN1ZSBzaGVldCB0cmFjayBvZmZzZXQgbXVzdCBiZSBldmVubHkgZGl2aXNpYmxlIGJ5IDU4OCBzYW1wbGVzAGN1ZSBzaGVldCB0cmFjayBtdXN0IGhhdmUgYXQgbGVhc3Qgb25lIGluZGV4IHBvaW50AGN1ZSBzaGVldCB0cmFjaydzIGZpcnN0IGluZGV4IG51bWJlciBtdXN0IGJlIDAgb3IgMQBDRC1EQSBjdWUgc2hlZXQgdHJhY2sgaW5kZXggb2Zmc2V0IG11c3QgYmUgZXZlbmx5IGRpdmlzaWJsZSBieSA1ODggc2FtcGxlcwBjdWUgc2hlZXQgdHJhY2sgaW5kZXggbnVtYmVycyBtdXN0IGluY3JlYXNlIGJ5IDEATUlNRSB0eXBlIHN0cmluZyBtdXN0IGNvbnRhaW4gb25seSBwcmludGFibGUgQVNDSUkgY2hhcmFjdGVycyAoMHgyMC0weDdlKQBkZXNjcmlwdGlvbiBzdHJpbmcgbXVzdCBiZSB2YWxpZCBVVEYtOA=="); -base64DecodeToExistingUint8Array(bufferView, 6516, "tx3BBG47ggnZJkMN3HYEE2trxReyTYYaBVBHHrjtCCYP8Mki1taKL2HLSytkmww104bNMQqgjjy9vU84cNsRTMfG0Ege4JNFqf1SQaytFV8bsNRbwpaXVnWLVlLINhlqfyvYbqYNm2MREFpnFEAdeaNd3H16e59wzWZedOC2I5hXq+Kcjo2hkTmQYJU8wCeLi93mj1L7pYLl5mSGWFsrvu9G6ro2YKm3gX1os4QtL60zMO6p6hatpF0LbKCQbTLUJ3Dz0P5WsN1JS3HZTBs2x/sG98MiILTOlT11yiiAOvKfnfv2Rru4+/Gmef/09j7hQ+v/5ZrNvOgt0H3sd3CGNMBtRzAZSwQ9rlbFOasGgiccG0MjxT0ALnIgwSrPnY4SeIBPFqGmDBsWu80fE+uKAaT2SwV90AgIys3JDAerl3iwtlZ8aZAVcd6N1HXb3ZNrbMBSb7XmEWIC+9Bmv0afXghbXlrRfR1XZmDcU2Mwm03ULVpJDQsZRLoW2ECXxqWsINtkqPn9J6VO4OahS7Chv/ytYLsliyO2kpbisi8rrYqYNmyOQRAvg/YN7ofzXamZREBonZ1mK5Aqe+qU5x204FAAdeSJJjbpPjv37TtrsPOMdnH3VVAy+uJN8/5f8LzG6O19wjHLPs+G1v/Lg4a41TSbedHtvTrcWqD72O7gDGlZ/c1tgNuOYDfGT2Qylgh6hYvJflytinPrsEt3Vg0ET+EQxUs4NoZGjytHQop7AFw9ZsFY5ECCVVNdQ1GeOx0lKSbcIfAAnyxHHV4oQk0ZNvVQ2DIsdps/m2taOybWFQORy9QHSO2XCv/wVg76oBEQTb3QFJSbkxkjhlIdDlYv8blL7vVgba3413Bs/NIgK+JlPermvBup6wsGaO+2uyfXAabm09iApd5vnWTaas0jxN3Q4sAE9qHNs+tgyX6NPr3JkP+5ELa8tKerfbCi+zquFeb7qszAuKd73XmjxmA2m3F995+oW7SSH0Z1lhoWMoitC/OMdC2wgcMwcYWZkIpdLo1LWferCFRAtslQReaOTvL7T0or3QxHnMDNQyF9gnuWYEN/T0YAcvhbwXb9C4ZoShZHbJMwBGEkLcVl6UubEV5WWhWHcBkYMG3YHDU9nwKCIF4GWwYdC+wb3A9RppM35rtSMz+dET6IgNA6jdCXJDrNViDj6xUtVPbUKXkmqcXOO2jBFx0rzKAA6silUK3WEk1s0strL998du7bwcuh43bWYOev8CPqGO3i7h29pfCqoGT0c4Yn+cSb5v0J/biJvuB5jWfGOoDQ2/uE1Yu8mmKWfZ67sD6TDK3/l7EQsK8GDXGr3ysypmg286JtZrS82nt1uANdNrW0QPexf0ZMQUMAAAAQAAAAiCoAAElEMw=="); -base64DecodeToExistingUint8Array(bufferView, 7576, "AQAAAAUAAAAYKw=="); -base64DecodeToExistingUint8Array(bufferView, 7600, "AwAAAAQAAAAEAAAABgAAAIP5ogBETm4A/CkVANFXJwDdNPUAYtvAADyZlQBBkEMAY1H+ALveqwC3YcUAOm4kANJNQgBJBuAACeouAByS0QDrHf4AKbEcAOg+pwD1NYIARLsuAJzphAC0JnAAQX5fANaROQBTgzkAnPQ5AItfhAAo+b0A+B87AN7/lwAPmAUAES/vAApaiwBtH20Az342AAnLJwBGT7cAnmY/AC3qXwC6J3UA5evHAD178QD3OQcAklKKAPtr6gAfsV8ACF2NADADVgB7/EYA8KtrACC8zwA29JoA46kdAF5hkQAIG+YAhZllAKAUXwCNQGgAgNj/ACdzTQAGBjEAylYVAMmocwB74mAAa4zAABnERwDNZ8MACejcAFmDKgCLdsQAphyWAESv3QAZV9EApT4FAAUH/wAzfj8AwjLoAJhP3gC7fTIAJj3DAB5r7wCf+F4ANR86AH/yygDxhx0AfJAhAGokfADVbvoAMC13ABU7QwC1FMYAwxmdAK3EwgAsTUEADABdAIZ9RgDjcS0Am8aaADNiAAC00nwAtKeXADdV1QDXPvYAoxAYAE12/ABknSoAcNerAGN8+AB6sFcAFxXnAMBJVgA71tkAp4Q4ACQjywDWincAWlQjAAAfuQDxChsAGc7fAJ8x/wBmHmoAmVdhAKz7RwB+f9gAImW3ADLoiQDmv2AA78TNAGw2CQBdP9QAFt7XAFg73gDem5IA0iIoACiG6ADiWE0AxsoyAAjjFgDgfcsAF8BQAPMdpwAY4FsALhM0AIMSYgCDSAEA9Y5bAK2wfwAe6fIASEpDABBn0wCq3dgArl9CAGphzgAKKKQA05m0AAam8gBcd38Ao8KDAGE8iACKc3gAr4xaAG/XvQAtpmMA9L/LAI2B7wAmwWcAVcpFAMrZNgAoqNIAwmGNABLJdwAEJhQAEkabAMRZxADIxUQATbKRAAAX8wDUQ60AKUnlAP3VEAAAvvwAHpTMAHDO7gATPvUA7PGAALPnwwDH+CgAkwWUAMFxPgAuCbMAC0XzAIgSnACrIHsALrWfAEeSwgB7Mi8ADFVtAHKnkABr5x8AMcuWAHkWSgBBeeIA9N+JAOiUlwDi5oQAmTGXAIjtawBfXzYAu/0OAEiatABnpGwAcXJCAI1dMgCfFbgAvOUJAI0xJQD3dDkAMAUcAA0MAQBLCGgALO5YAEeqkAB05wIAvdYkAPd9pgBuSHIAnxbvAI6UpgC0kfYA0VNRAM8K8gAgmDMA9Ut+ALJjaADdPl8AQF0DAIWJfwBVUikAN2TAAG3YEAAySDIAW0x1AE5x1ABFVG4ACwnBACr1aQAUZtUAJwedAF0EUAC0O9sA6nbFAIf5FwBJa30AHSe6AJZpKQDGzKwArRRUAJDiagCI2YkALHJQAASkvgB3B5QA8zBwAAD8JwDqcagAZsJJAGTgPQCX3YMAoz+XAEOU/QANhowAMUHeAJI5nQDdcIwAF7fnAAjfOwAVNysAXICgAFqAkwAQEZIAD+jYAGyArwDb/0sAOJAPAFkYdgBipRUAYcu7AMeJuQAQQL0A0vIEAEl1JwDrtvYA2yK7AAoUqgCJJi8AZIN2AAk7MwAOlBoAUTqqAB2jwgCv7a4AXCYSAG3CTQAtepwAwFaXAAM/gwAJ8PYAK0CMAG0xmQA5tAcADCAVANjDWwD1ksQAxq1LAE7KpQCnN80A5qk2AKuSlADdQmgAGWPeAHaM7wBoi1IA/Ns3AK6hqwDfFTEAAK6hAAz72gBkTWYA7QW3ACllMABXVr8AR/86AGr5uQB1vvMAKJPfAKuAMABmjPYABMsVAPoiBgDZ5B0APbOkAFcbjwA2zQkATkLpABO+pAAzI7UA8KoaAE9lqADSwaUACz8PAFt4zQAj+XYAe4sEAIkXcgDGplMAb27iAO/rAACbSlgAxNq3AKpmugB2z88A0QIdALHxLQCMmcEAw613AIZI2gD3XaAAxoD0AKzwLwDd7JoAP1y8ANDebQCQxx8AKtu2AKMlOgAAr5oArVOTALZXBAApLbQAS4B+ANoHpwB2qg4Ae1mhABYSKgDcty0A+uX9AInb/gCJvv0A5HZsAAap/AA+gHAAhW4VAP2H/wAoPgcAYWczACoYhgBNveoAs+evAI9tbgCVZzkAMb9bAITXSAAw3xYAxy1DACVhNQDJcM4AMMu4AL9s/QCkAKIABWzkAFrdoAAhb0cAYhLSALlchABwYUkAa1bgAJlSAQBQVTcAHtW3ADPxxAATbl8AXTDkAIUuqQAdssMAoTI2AAi3pADqsdQAFvchAI9p5AAn/3cADAOAAI1ALQBPzaAAIKWZALOi0wAvXQoAtPlCABHaywB9vtAAm9vBAKsXvQDKooEACGpcAC5VFwAnAFUAfxTwAOEHhgAUC2QAlkGNAIe+3gDa/SoAayW2AHuJNAAF8/4Aub+eAGhqTwBKKqgAT8RaAC34vADXWpgA9MeVAA1NjQAgOqYApFdfABQ/sQCAOJUAzCABAHHdhgDJ3rYAv2D1AE1lEQABB2sAjLCsALLA0ABRVUgAHvsOAJVywwCjBjsAwEA1AAbcewDgRcwATin6ANbKyADo80EAfGTeAJtk2ADZvjEApJfDAHdY1ABp48UA8NoTALo6PABGGEYAVXVfANK99QBuksYArC5dAA5E7QAcPkIAYcSHACn96QDn1vMAInzKAG+RNQAI4MUA/9eNAG5q4gCw/cYAkwjBAHxddABrrbIAzW6dAD5yewDGEWoA98+pAClz3wC1yboAtwBRAOKyDQB0uiQA5X1gAHTYigANFSwAgRgMAH5mlAABKRYAn3p2AP39vgBWRe8A2X42AOzZEwCLurkAxJf8ADGoJwDxbsMAlMU2ANioVgC0qLUAz8wOABKJLQBvVzQALFaJAJnO4wDWILkAa16qAD4qnAARX8wA/QtKAOH0+wCOO20A4oYsAOnUhAD8tKkA7+7RAC41yQAvOWEAOCFEABvZyACB/AoA+0pqAC8c2ABTtIQATpmMAFQizAAqVdwAwMbWAAsZlgAacLgAaZVkACZaYAA/Uu4AfxEPAPS1EQD8y/UANLwtADS87gDoXcwA3V5gAGeOmwCSM+8AyRe4AGFYmwDhV7wAUYPGANg+EADdcUgALRzdAK8YoQAhLEYAWfPXANl6mACeVMAAT4b6AFYG/ADlea4AiSI2ADitIgBnk9wAVeiqAIImOADK55sAUQ2kAJkzsQCp1w4AaQVIAGWy8AB/iKcAiEyXAPnRNgAhkrMAe4JKAJjPIQBAn9wA3EdVAOF0OgBn60IA/p3fAF7UXwB7Z6QAuqx6AFX2ogAriCMAQbpVAFluCAAhKoYAOUeDAInj5gDlntQASftAAP9W6QAcD8oAxVmKAJT6KwDTwcUAD8XPANtargBHxYYAhUNiACGGOwAseZQAEGGHACpMewCALBoAQ78SAIgmkAB4PIkAqMTkAOXbewDEOsIAJvTqAPdnigANkr8AZaMrAD2TsQC9fAsApFHcACfdYwBp4d0AmpQZAKgplQBozigACe20AESfIABOmMoAcIJjAH58IwAPuTIAp/WOABRW5wAh8QgAtZ0qAG9+TQClGVEAtfmrAILf1gCW3WEAFjYCAMQ6nwCDoqEAcu1tADmNegCCuKkAazJcAEYnWwAANO0A0gB3APz0VQABWU0A4HGA"); -base64DecodeToExistingUint8Array(bufferView, 10387, "QPsh+T8AAAAALUR0PgAAAICYRvg8AAAAYFHMeDsAAACAgxvwOQAAAEAgJXo4AAAAgCKC4zYAAAAAHfNpNQAAAAAAAOA/AAAAAAAA4L8BAAAAAgAAAAQAAAAFAAAABgAAAGluZmluaXR5AG5hbg=="); -base64DecodeToExistingUint8Array(bufferView, 10512, "0XSeAFedvSqAcFIP//8+JwoAAABkAAAA6AMAABAnAACghgEAQEIPAICWmAAA4fUFGAAAADUAAABxAAAAa////877//+Sv///YmFydGxldHQAYmFydGxldHRfaGFubgBibGFja21hbgBibGFja21hbl9oYXJyaXNfNHRlcm1fOTJkYgBjb25uZXMAZmxhdHRvcABnYXVzcygAaGFtbWluZwBoYW5uAGthaXNlcl9iZXNzZWwAbnV0dGFsbAByZWN0YW5nbGUAdHJpYW5nbGUAdHVrZXkoAHBhcnRpYWxfdHVrZXkoAHB1bmNob3V0X3R1a2V5KAB3ZWxjaABpbWFnZS9wbmcALS0+AHR1a2V5KDVlLTEpAHR1a2V5KDVlLTEpO3BhcnRpYWxfdHVrZXkoMikAdHVrZXkoNWUtMSk7cGFydGlhbF90dWtleSgyKTtwdW5jaG91dF90dWtleSgzKQ=="); -base64DecodeToExistingUint8Array(bufferView, 10881, "FQAAcR0AAAk="); -base64DecodeToExistingUint8Array(bufferView, 10900, "Ag=="); -base64DecodeToExistingUint8Array(bufferView, 10920, "AwAAAAAAAAAEAAAASC8AAAAE"); -base64DecodeToExistingUint8Array(bufferView, 10964, "/////w=="); -base64DecodeToExistingUint8Array(bufferView, 11032, "BQ=="); -base64DecodeToExistingUint8Array(bufferView, 11044, "CQ=="); -base64DecodeToExistingUint8Array(bufferView, 11068, "CgAAAAsAAABYMwAAAAQ="); -base64DecodeToExistingUint8Array(bufferView, 11092, "AQ=="); -base64DecodeToExistingUint8Array(bufferView, 11107, "Cv////8="); -base64DecodeToExistingUint8Array(bufferView, 11176, "GCs="); -base64DecodeToExistingUint8Array(bufferView, 11216, "AwAAAAAAAAAZKgAAAQAAAAE="); -base64DecodeToExistingUint8Array(bufferView, 11260, "AwAAAAAAAAAZKgAAAQ=="); -base64DecodeToExistingUint8Array(bufferView, 11304, "AwAAAAAAAAAZKg=="); -base64DecodeToExistingUint8Array(bufferView, 11324, "Bg=="); -base64DecodeToExistingUint8Array(bufferView, 11348, "BAAAAAAAAAAZKgAAAQAAAAEAAAAI"); -base64DecodeToExistingUint8Array(bufferView, 11392, "BAAAAAAAAAAZKgAAAQAAAAAAAAAI"); -base64DecodeToExistingUint8Array(bufferView, 11436, "BQAAAAAAAAAZKgAAAQAAAAAAAAAI"); -base64DecodeToExistingUint8Array(bufferView, 11480, "BgAAAAAAAAAlKgAAAQAAAAAAAAAM"); -base64DecodeToExistingUint8Array(bufferView, 11524, "BgAAAAAAAAAlKgAAAQAAAAAAAAAM"); -base64DecodeToExistingUint8Array(bufferView, 11568, "BgAAAAAAAABCKg=="); -return asmFunc({ - 'Int8Array': Int8Array, - 'Int16Array': Int16Array, - 'Int32Array': Int32Array, - 'Uint8Array': Uint8Array, - 'Uint16Array': Uint16Array, - 'Uint32Array': Uint32Array, - 'Float32Array': Float32Array, - 'Float64Array': Float64Array, - 'NaN': NaN, - 'Infinity': Infinity, - 'Math': Math - }, - asmLibraryArg, - wasmMemory.buffer -) - -} -)(asmLibraryArg, wasmMemory, wasmTable); - }, - - instantiate: /** @suppress{checkTypes} */ function(binary, info) { - return { - then: function(ok) { - ok({ - 'instance': new WebAssembly.Instance(new WebAssembly.Module(binary)) - }); - } - }; - }, - - RuntimeError: Error -}; - -// We don't need to actually download a wasm binary, mark it as present but empty. -wasmBinary = []; - - - -if (typeof WebAssembly !== 'object') { - abort('no native wasm support detected'); -} - - - - -// In MINIMAL_RUNTIME, setValue() and getValue() are only available when building with safe heap enabled, for heap safety checking. -// In traditional runtime, setValue() and getValue() are always available (although their use is highly discouraged due to perf penalties) - -/** @param {number} ptr - @param {number} value - @param {string} type - @param {number|boolean=} noSafe */ -function setValue(ptr, value, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit - switch(type) { - case 'i1': HEAP8[((ptr)>>0)]=value; break; - case 'i8': HEAP8[((ptr)>>0)]=value; break; - case 'i16': HEAP16[((ptr)>>1)]=value; break; - case 'i32': HEAP32[((ptr)>>2)]=value; break; - case 'i64': (tempI64 = [value>>>0,(tempDouble=value,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((ptr)>>2)]=tempI64[0],HEAP32[(((ptr)+(4))>>2)]=tempI64[1]); break; - case 'float': HEAPF32[((ptr)>>2)]=value; break; - case 'double': HEAPF64[((ptr)>>3)]=value; break; - default: abort('invalid type for setValue: ' + type); - } -} - -/** @param {number} ptr - @param {string} type - @param {number|boolean=} noSafe */ -function getValue(ptr, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit - switch(type) { - case 'i1': return HEAP8[((ptr)>>0)]; - case 'i8': return HEAP8[((ptr)>>0)]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP32[((ptr)>>2)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - default: abort('invalid type for getValue: ' + type); - } - return null; -} - - - - - - -// Wasm globals - -var wasmMemory; - -// In fastcomp asm.js, we don't need a wasm Table at all. -// In the wasm backend, we polyfill the WebAssembly object, -// so this creates a (non-native-wasm) table for us. -var wasmTable = new WebAssembly.Table({ - 'initial': 22, - 'maximum': 22 + 5, - 'element': 'anyfunc' -}); - - -//======================================== -// Runtime essentials -//======================================== - -// whether we are quitting the application. no code should run after this. -// set in exit() and abort() -var ABORT = false; - -// set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code code in shell environments -// but only when noExitRuntime is false. -var EXITSTATUS = 0; - -/** @type {function(*, string=)} */ -function assert(condition, text) { - if (!condition) { - abort('Assertion failed: ' + text); - } -} - -// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) -function getCFunc(ident) { - var func = Module['_' + ident]; // closure exported function - assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); - return func; -} - -// C calling interface. -/** @param {string|null=} returnType - @param {Array=} argTypes - @param {Arguments|Array=} args - @param {Object=} opts */ -function ccall(ident, returnType, argTypes, args, opts) { - // For fast lookup of conversion functions - var toC = { - 'string': function(str) { - var ret = 0; - if (str !== null && str !== undefined && str !== 0) { // null string - // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' - var len = (str.length << 2) + 1; - ret = stackAlloc(len); - stringToUTF8(str, ret, len); - } - return ret; - }, - 'array': function(arr) { - var ret = stackAlloc(arr.length); - writeArrayToMemory(arr, ret); - return ret; - } - }; - - function convertReturnValue(ret) { - if (returnType === 'string') return UTF8ToString(ret); - if (returnType === 'boolean') return Boolean(ret); - return ret; - } - - var func = getCFunc(ident); - var cArgs = []; - var stack = 0; - if (args) { - for (var i = 0; i < args.length; i++) { - var converter = toC[argTypes[i]]; - if (converter) { - if (stack === 0) stack = stackSave(); - cArgs[i] = converter(args[i]); - } else { - cArgs[i] = args[i]; - } - } - } - var ret = func.apply(null, cArgs); - - ret = convertReturnValue(ret); - if (stack !== 0) stackRestore(stack); - return ret; -} - -/** @param {string=} returnType - @param {Array=} argTypes - @param {Object=} opts */ -function cwrap(ident, returnType, argTypes, opts) { - argTypes = argTypes || []; - // When the function takes numbers and returns a number, we can just return - // the original function - var numericArgs = argTypes.every(function(type){ return type === 'number'}); - var numericRet = returnType !== 'string'; - if (numericRet && numericArgs && !opts) { - return getCFunc(ident); - } - return function() { - return ccall(ident, returnType, argTypes, arguments, opts); - } -} - -var ALLOC_NORMAL = 0; // Tries to use _malloc() -var ALLOC_STACK = 1; // Lives for the duration of the current function call -var ALLOC_DYNAMIC = 2; // Cannot be freed except through sbrk -var ALLOC_NONE = 3; // Do not allocate - -// allocate(): This is for internal use. You can use it yourself as well, but the interface -// is a little tricky (see docs right below). The reason is that it is optimized -// for multiple syntaxes to save space in generated code. So you should -// normally not use allocate(), and instead allocate memory using _malloc(), -// initialize it with setValue(), and so forth. -// @slab: An array of data, or a number. If a number, then the size of the block to allocate, -// in *bytes* (note that this is sometimes confusing: the next parameter does not -// affect this!) -// @types: Either an array of types, one for each byte (or 0 if no type at that position), -// or a single type which is used for the entire block. This only matters if there -// is initial data - if @slab is a number, then this does not matter at all and is -// ignored. -// @allocator: How to allocate memory, see ALLOC_* -/** @type {function((TypedArray|Array|number), string, number, number=)} */ -function allocate(slab, types, allocator, ptr) { - var zeroinit, size; - if (typeof slab === 'number') { - zeroinit = true; - size = slab; - } else { - zeroinit = false; - size = slab.length; - } - - var singleType = typeof types === 'string' ? types : null; - - var ret; - if (allocator == ALLOC_NONE) { - ret = ptr; - } else { - ret = [_malloc, - stackAlloc, - dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)); - } - - if (zeroinit) { - var stop; - ptr = ret; - assert((ret & 3) == 0); - stop = ret + (size & ~3); - for (; ptr < stop; ptr += 4) { - HEAP32[((ptr)>>2)]=0; - } - stop = ret + size; - while (ptr < stop) { - HEAP8[((ptr++)>>0)]=0; - } - return ret; - } - - if (singleType === 'i8') { - if (slab.subarray || slab.slice) { - HEAPU8.set(/** @type {!Uint8Array} */ (slab), ret); - } else { - HEAPU8.set(new Uint8Array(slab), ret); - } - return ret; - } - - var i = 0, type, typeSize, previousType; - while (i < size) { - var curr = slab[i]; - - type = singleType || types[i]; - if (type === 0) { - i++; - continue; - } - - if (type == 'i64') type = 'i32'; // special case: we have one i32 here, and one i32 later - - setValue(ret+i, curr, type); - - // no need to look up size unless type changes, so cache it - if (previousType !== type) { - typeSize = getNativeTypeSize(type); - previousType = type; - } - i += typeSize; - } - - return ret; -} - -// Allocate memory during any stage of startup - static memory early on, dynamic memory later, malloc when ready -function getMemory(size) { - if (!runtimeInitialized) return dynamicAlloc(size); - return _malloc(size); -} - - - - -// runtime_strings.js: Strings related runtime functions that are part of both MINIMAL_RUNTIME and regular runtime. - -// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the given array that contains uint8 values, returns -// a copy of that string as a Javascript String object. - -var UTF8Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf8') : undefined; - -/** - * @param {number} idx - * @param {number=} maxBytesToRead - * @return {string} - */ -function UTF8ArrayToString(heap, idx, maxBytesToRead) { - var endIdx = idx + maxBytesToRead; - var endPtr = idx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. - // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. - // (As a tiny code save trick, compare endPtr against endIdx using a negation, so that undefined means Infinity) - while (heap[endPtr] && !(endPtr >= endIdx)) ++endPtr; - - if (endPtr - idx > 16 && heap.subarray && UTF8Decoder) { - return UTF8Decoder.decode(heap.subarray(idx, endPtr)); - } else { - var str = ''; - // If building with TextDecoder, we have already computed the string length above, so test loop end condition against that - while (idx < endPtr) { - // For UTF8 byte structure, see: - // http://en.wikipedia.org/wiki/UTF-8#Description - // https://www.ietf.org/rfc/rfc2279.txt - // https://tools.ietf.org/html/rfc3629 - var u0 = heap[idx++]; - if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } - var u1 = heap[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } - var u2 = heap[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heap[idx++] & 63); - } - - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - var ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - } - return str; -} - -// Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the emscripten HEAP, returns a -// copy of that string as a Javascript String object. -// maxBytesToRead: an optional length that specifies the maximum number of bytes to read. You can omit -// this parameter to scan the string until the first \0 byte. If maxBytesToRead is -// passed, and the string at [ptr, ptr+maxBytesToReadr[ contains a null byte in the -// middle, then the string will cut short at that byte index (i.e. maxBytesToRead will -// not produce a string of exact length [ptr, ptr+maxBytesToRead[) -// N.B. mixing frequent uses of UTF8ToString() with and without maxBytesToRead may -// throw JS JIT optimizations off, so it is worth to consider consistently using one -// style or the other. -/** - * @param {number} ptr - * @param {number=} maxBytesToRead - * @return {string} - */ -function UTF8ToString(ptr, maxBytesToRead) { - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; -} - -// Copies the given Javascript String object 'str' to the given byte array at address 'outIdx', -// encoded in UTF8 form and null-terminated. The copy will require at most str.length*4+1 bytes of space in the HEAP. -// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// heap: the array to copy to. Each index in this array is assumed to be one 8-byte element. -// outIdx: The starting offset in the array to begin the copying. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. -// This count should include the null terminator, -// i.e. if maxBytesToWrite=1, only the null terminator will be written and nothing else. -// maxBytesToWrite=0 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) { - if (!(maxBytesToWrite > 0)) // Parameter maxBytesToWrite is not optional. Negative values, 0, null, undefined and false each don't write out any bytes. - return 0; - - var startIdx = outIdx; - var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description and https://www.ietf.org/rfc/rfc2279.txt and https://tools.ietf.org/html/rfc3629 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) { - var u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } - if (u <= 0x7F) { - if (outIdx >= endIdx) break; - heap[outIdx++] = u; - } else if (u <= 0x7FF) { - if (outIdx + 1 >= endIdx) break; - heap[outIdx++] = 0xC0 | (u >> 6); - heap[outIdx++] = 0x80 | (u & 63); - } else if (u <= 0xFFFF) { - if (outIdx + 2 >= endIdx) break; - heap[outIdx++] = 0xE0 | (u >> 12); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } else { - if (outIdx + 3 >= endIdx) break; - heap[outIdx++] = 0xF0 | (u >> 18); - heap[outIdx++] = 0x80 | ((u >> 12) & 63); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } - } - // Null-terminate the pointer to the buffer. - heap[outIdx] = 0; - return outIdx - startIdx; -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF8 form. The copy will require at most str.length*4+1 bytes of space in the HEAP. -// Use the function lengthBytesUTF8 to compute the exact number of bytes (excluding null terminator) that this function will write. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF8(str, outPtr, maxBytesToWrite) { - return stringToUTF8Array(str, HEAPU8,outPtr, maxBytesToWrite); -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF8 byte array, EXCLUDING the null terminator byte. -function lengthBytesUTF8(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - if (u <= 0x7F) ++len; - else if (u <= 0x7FF) len += 2; - else if (u <= 0xFFFF) len += 3; - else len += 4; - } - return len; -} - - - - - -// runtime_strings_extra.js: Strings related runtime functions that are available only in regular runtime. - -// Given a pointer 'ptr' to a null-terminated ASCII-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. - -function AsciiToString(ptr) { - var str = ''; - while (1) { - var ch = HEAPU8[((ptr++)>>0)]; - if (!ch) return str; - str += String.fromCharCode(ch); - } -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in ASCII form. The copy will require at most str.length+1 bytes of space in the HEAP. - -function stringToAscii(str, outPtr) { - return writeAsciiToMemory(str, outPtr, false); -} - -// Given a pointer 'ptr' to a null-terminated UTF16LE-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. - -var UTF16Decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : undefined; - -function UTF16ToString(ptr, maxBytesToRead) { - var endPtr = ptr; - // TextDecoder needs to know the byte length in advance, it doesn't stop on null terminator by itself. - // Also, use the length info to avoid running tiny strings through TextDecoder, since .subarray() allocates garbage. - var idx = endPtr >> 1; - var maxIdx = idx + maxBytesToRead / 2; - // If maxBytesToRead is not passed explicitly, it will be undefined, and this - // will always evaluate to true. This saves on code size. - while (!(idx >= maxIdx) && HEAPU16[idx]) ++idx; - endPtr = idx << 1; - - if (endPtr - ptr > 32 && UTF16Decoder) { - return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); - } else { - var i = 0; - - var str = ''; - while (1) { - var codeUnit = HEAP16[(((ptr)+(i*2))>>1)]; - if (codeUnit == 0 || i == maxBytesToRead / 2) return str; - ++i; - // fromCharCode constructs a character from a UTF-16 code unit, so we can pass the UTF16 string right through. - str += String.fromCharCode(codeUnit); - } - } -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF16 form. The copy will require at most str.length*4+2 bytes of space in the HEAP. -// Use the function lengthBytesUTF16() to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// outPtr: Byte address in Emscripten HEAP where to write the string to. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null -// terminator, i.e. if maxBytesToWrite=2, only the null terminator will be written and nothing else. -// maxBytesToWrite<2 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF16(str, outPtr, maxBytesToWrite) { - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - if (maxBytesToWrite === undefined) { - maxBytesToWrite = 0x7FFFFFFF; - } - if (maxBytesToWrite < 2) return 0; - maxBytesToWrite -= 2; // Null terminator. - var startPtr = outPtr; - var numCharsToWrite = (maxBytesToWrite < str.length*2) ? (maxBytesToWrite / 2) : str.length; - for (var i = 0; i < numCharsToWrite; ++i) { - // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. - var codeUnit = str.charCodeAt(i); // possibly a lead surrogate - HEAP16[((outPtr)>>1)]=codeUnit; - outPtr += 2; - } - // Null-terminate the pointer to the HEAP. - HEAP16[((outPtr)>>1)]=0; - return outPtr - startPtr; -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. - -function lengthBytesUTF16(str) { - return str.length*2; -} - -function UTF32ToString(ptr, maxBytesToRead) { - var i = 0; - - var str = ''; - // If maxBytesToRead is not passed explicitly, it will be undefined, and this - // will always evaluate to true. This saves on code size. - while (!(i >= maxBytesToRead / 4)) { - var utf32 = HEAP32[(((ptr)+(i*4))>>2)]; - if (utf32 == 0) break; - ++i; - // Gotcha: fromCharCode constructs a character from a UTF-16 encoded code (pair), not from a Unicode code point! So encode the code point to UTF-16 for constructing. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - if (utf32 >= 0x10000) { - var ch = utf32 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } else { - str += String.fromCharCode(utf32); - } - } - return str; -} - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF32 form. The copy will require at most str.length*4+4 bytes of space in the HEAP. -// Use the function lengthBytesUTF32() to compute the exact number of bytes (excluding null terminator) that this function will write. -// Parameters: -// str: the Javascript string to copy. -// outPtr: Byte address in Emscripten HEAP where to write the string to. -// maxBytesToWrite: The maximum number of bytes this function can write to the array. This count should include the null -// terminator, i.e. if maxBytesToWrite=4, only the null terminator will be written and nothing else. -// maxBytesToWrite<4 does not write any bytes to the output, not even the null terminator. -// Returns the number of bytes written, EXCLUDING the null terminator. - -function stringToUTF32(str, outPtr, maxBytesToWrite) { - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - if (maxBytesToWrite === undefined) { - maxBytesToWrite = 0x7FFFFFFF; - } - if (maxBytesToWrite < 4) return 0; - var startPtr = outPtr; - var endPtr = startPtr + maxBytesToWrite - 4; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var codeUnit = str.charCodeAt(i); // possibly a lead surrogate - if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) { - var trailSurrogate = str.charCodeAt(++i); - codeUnit = 0x10000 + ((codeUnit & 0x3FF) << 10) | (trailSurrogate & 0x3FF); - } - HEAP32[((outPtr)>>2)]=codeUnit; - outPtr += 4; - if (outPtr + 4 > endPtr) break; - } - // Null-terminate the pointer to the HEAP. - HEAP32[((outPtr)>>2)]=0; - return outPtr - startPtr; -} - -// Returns the number of bytes the given Javascript string takes if encoded as a UTF16 byte array, EXCLUDING the null terminator byte. - -function lengthBytesUTF32(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var codeUnit = str.charCodeAt(i); - if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) ++i; // possibly a lead surrogate, so skip over the tail surrogate. - len += 4; - } - - return len; -} - -// Allocate heap space for a JS string, and write it there. -// It is the responsibility of the caller to free() that memory. -function allocateUTF8(str) { - var size = lengthBytesUTF8(str) + 1; - var ret = _malloc(size); - if (ret) stringToUTF8Array(str, HEAP8, ret, size); - return ret; -} - -// Allocate stack space for a JS string, and write it there. -function allocateUTF8OnStack(str) { - var size = lengthBytesUTF8(str) + 1; - var ret = stackAlloc(size); - stringToUTF8Array(str, HEAP8, ret, size); - return ret; -} - -// Deprecated: This function should not be called because it is unsafe and does not provide -// a maximum length limit of how many bytes it is allowed to write. Prefer calling the -// function stringToUTF8Array() instead, which takes in a maximum length that can be used -// to be secure from out of bounds writes. -/** @deprecated - @param {boolean=} dontAddNull */ -function writeStringToMemory(string, buffer, dontAddNull) { - warnOnce('writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!'); - - var /** @type {number} */ lastChar, /** @type {number} */ end; - if (dontAddNull) { - // stringToUTF8Array always appends null. If we don't want to do that, remember the - // character that existed at the location where the null will be placed, and restore - // that after the write (below). - end = buffer + lengthBytesUTF8(string); - lastChar = HEAP8[end]; - } - stringToUTF8(string, buffer, Infinity); - if (dontAddNull) HEAP8[end] = lastChar; // Restore the value under the null character. -} - -function writeArrayToMemory(array, buffer) { - HEAP8.set(array, buffer); -} - -/** @param {boolean=} dontAddNull */ -function writeAsciiToMemory(str, buffer, dontAddNull) { - for (var i = 0; i < str.length; ++i) { - HEAP8[((buffer++)>>0)]=str.charCodeAt(i); - } - // Null-terminate the pointer to the HEAP. - if (!dontAddNull) HEAP8[((buffer)>>0)]=0; -} - - - -// Memory management - -var PAGE_SIZE = 16384; -var WASM_PAGE_SIZE = 65536; -var ASMJS_PAGE_SIZE = 16777216; - -function alignUp(x, multiple) { - if (x % multiple > 0) { - x += multiple - (x % multiple); - } - return x; -} - -var HEAP, -/** @type {ArrayBuffer} */ - buffer, -/** @type {Int8Array} */ - HEAP8, -/** @type {Uint8Array} */ - HEAPU8, -/** @type {Int16Array} */ - HEAP16, -/** @type {Uint16Array} */ - HEAPU16, -/** @type {Int32Array} */ - HEAP32, -/** @type {Uint32Array} */ - HEAPU32, -/** @type {Float32Array} */ - HEAPF32, -/** @type {Float64Array} */ - HEAPF64; - -function updateGlobalBufferAndViews(buf) { - buffer = buf; - Module['HEAP8'] = HEAP8 = new Int8Array(buf); - Module['HEAP16'] = HEAP16 = new Int16Array(buf); - Module['HEAP32'] = HEAP32 = new Int32Array(buf); - Module['HEAPU8'] = HEAPU8 = new Uint8Array(buf); - Module['HEAPU16'] = HEAPU16 = new Uint16Array(buf); - Module['HEAPU32'] = HEAPU32 = new Uint32Array(buf); - Module['HEAPF32'] = HEAPF32 = new Float32Array(buf); - Module['HEAPF64'] = HEAPF64 = new Float64Array(buf); -} - -var STATIC_BASE = 1024, - STACK_BASE = 5257216, - STACKTOP = STACK_BASE, - STACK_MAX = 14336, - DYNAMIC_BASE = 5257216, - DYNAMICTOP_PTR = 14176; - - - -var TOTAL_STACK = 5242880; - -var INITIAL_INITIAL_MEMORY = Module['INITIAL_MEMORY'] || 16777216; - - - - - - - - - -// In non-standalone/normal mode, we create the memory here. - - - -// Create the main memory. (Note: this isn't used in STANDALONE_WASM mode since the wasm -// memory is created in the wasm, not in JS.) - - if (Module['wasmMemory']) { - wasmMemory = Module['wasmMemory']; - } else - { - wasmMemory = new WebAssembly.Memory({ - 'initial': INITIAL_INITIAL_MEMORY / WASM_PAGE_SIZE - , - 'maximum': 2147483648 / WASM_PAGE_SIZE - }); - } - - -if (wasmMemory) { - buffer = wasmMemory.buffer; -} - -// If the user provides an incorrect length, just use that length instead rather than providing the user to -// specifically provide the memory length with Module['INITIAL_MEMORY']. -INITIAL_INITIAL_MEMORY = buffer.byteLength; -updateGlobalBufferAndViews(buffer); - -HEAP32[DYNAMICTOP_PTR>>2] = DYNAMIC_BASE; - - - - - - - - - - - - - - -function callRuntimeCallbacks(callbacks) { - while(callbacks.length > 0) { - var callback = callbacks.shift(); - if (typeof callback == 'function') { - callback(Module); // Pass the module as the first argument. - continue; - } - var func = callback.func; - if (typeof func === 'number') { - if (callback.arg === undefined) { - Module['dynCall_v'](func); - } else { - Module['dynCall_vi'](func, callback.arg); - } - } else { - func(callback.arg === undefined ? null : callback.arg); - } - } -} - -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATMAIN__ = []; // functions called when main() is to be run -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the main() is called - -var runtimeInitialized = false; -var runtimeExited = false; - - -function preRun() { - - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - - callRuntimeCallbacks(__ATPRERUN__); -} - -function initRuntime() { - runtimeInitialized = true; - if (!Module["noFSInit"] && !FS.init.initialized) FS.init(); -TTY.init(); - callRuntimeCallbacks(__ATINIT__); -} - -function preMain() { - FS.ignorePermissions = false; - callRuntimeCallbacks(__ATMAIN__); -} - -function exitRuntime() { - runtimeExited = true; -} - -function postRun() { - - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} - -function addOnPreMain(cb) { - __ATMAIN__.unshift(cb); -} - -function addOnExit(cb) { -} - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} - -/** @param {number|boolean=} ignore */ -function unSign(value, bits, ignore) { - if (value >= 0) { - return value; - } - return bits <= 32 ? 2*Math.abs(1 << (bits-1)) + value // Need some trickery, since if bits == 32, we are right at the limit of the bits JS uses in bitshifts - : Math.pow(2, bits) + value; -} -/** @param {number|boolean=} ignore */ -function reSign(value, bits, ignore) { - if (value <= 0) { - return value; - } - var half = bits <= 32 ? Math.abs(1 << (bits-1)) // abs is needed if bits == 32 - : Math.pow(2, bits-1); - if (value >= half && (bits <= 32 || value > half)) { // for huge values, we can hit the precision limit and always get true here. so don't do that - // but, in general there is no perfect solution here. With 64-bit ints, we get rounding and errors - // TODO: In i64 mode 1, resign the two parts separately and safely - value = -2*half + value; // Cannot bitshift half, as it may be at the limit of the bits JS uses in bitshifts - } - return value; -} - - - - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc - - -var Math_abs = Math.abs; -var Math_cos = Math.cos; -var Math_sin = Math.sin; -var Math_tan = Math.tan; -var Math_acos = Math.acos; -var Math_asin = Math.asin; -var Math_atan = Math.atan; -var Math_atan2 = Math.atan2; -var Math_exp = Math.exp; -var Math_log = Math.log; -var Math_sqrt = Math.sqrt; -var Math_ceil = Math.ceil; -var Math_floor = Math.floor; -var Math_pow = Math.pow; -var Math_imul = Math.imul; -var Math_fround = Math.fround; -var Math_round = Math.round; -var Math_min = Math.min; -var Math_max = Math.max; -var Math_clz32 = Math.clz32; -var Math_trunc = Math.trunc; - - - -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// Module.preRun (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled - -function getUniqueRunDependency(id) { - return id; -} - -function addRunDependency(id) { - runDependencies++; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - -} - -function removeRunDependency(id) { - runDependencies--; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } -} - -Module["preloadedImages"] = {}; // maps url to image data -Module["preloadedAudios"] = {}; // maps url to audio data - -/** @param {string|number=} what */ -function abort(what) { - if (Module['onAbort']) { - Module['onAbort'](what); - } - - what += ''; - out(what); - err(what); - - ABORT = true; - EXITSTATUS = 1; - - what = 'abort(' + what + '). Build with -s ASSERTIONS=1 for more info.'; - - // Throw a wasm runtime error, because a JS error might be seen as a foreign - // exception, which means we'd run destructors on it. We need the error to - // simply make the program stop. - throw new WebAssembly.RuntimeError(what); -} - - -var memoryInitializer = null; - - - - - - - - - - - - -function hasPrefix(str, prefix) { - return String.prototype.startsWith ? - str.startsWith(prefix) : - str.indexOf(prefix) === 0; -} - -// Prefix of data URIs emitted by SINGLE_FILE and related options. -var dataURIPrefix = 'data:application/octet-stream;base64,'; - -// Indicates whether filename is a base64 data URI. -function isDataURI(filename) { - return hasPrefix(filename, dataURIPrefix); -} - -var fileURIPrefix = "file://"; - -// Indicates whether filename is delivered via file protocol (as opposed to http/https) -function isFileURI(filename) { - return hasPrefix(filename, fileURIPrefix); -} - - - - -var wasmBinaryFile = 'libflac.wasm'; -if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); -} - -function getBinary() { - try { - if (wasmBinary) { - return new Uint8Array(wasmBinary); - } - - var binary = tryParseAsDataURI(wasmBinaryFile); - if (binary) { - return binary; - } - if (readBinary) { - return readBinary(wasmBinaryFile); - } else { - throw "both async and sync fetching of the wasm failed"; - } - } - catch (err) { - abort(err); - } -} - -function getBinaryPromise() { - // If we don't have the binary yet, and have the Fetch api, use that; - // in some environments, like Electron's render process, Fetch api may be present, but have a different context than expected, let's only use it on the Web - if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === 'function' - // Let's not use fetch to get objects over file:// as it's most likely Cordova which doesn't support fetch for file:// - && !isFileURI(wasmBinaryFile) - ) { - return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) { - if (!response['ok']) { - throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; - } - return response['arrayBuffer'](); - }).catch(function () { - return getBinary(); - }); - } - // Otherwise, getBinary should be able to get it synchronously - return new Promise(function(resolve, reject) { - resolve(getBinary()); - }); -} - - - -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm() { - // prepare imports - var info = { - 'env': asmLibraryArg, - 'wasi_snapshot_preview1': asmLibraryArg - }; - // Load the wasm module and create an instance of using native support in the JS engine. - // handle a generated wasm instance, receiving its exports and - // performing other necessary setup - /** @param {WebAssembly.Module=} module*/ - function receiveInstance(instance, module) { - var exports = instance.exports; - Module['asm'] = exports; - removeRunDependency('wasm-instantiate'); - } - // we can't run yet (except in a pthread, where we have a custom sync instantiator) - addRunDependency('wasm-instantiate'); - - - function receiveInstantiatedSource(output) { - // 'output' is a WebAssemblyInstantiatedSource object which has both the module and instance. - // receiveInstance() will swap in the exports (to Module.asm) so they can be called - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. - // When the regression is fixed, can restore the above USE_PTHREADS-enabled path. - receiveInstance(output['instance']); - } - - - function instantiateArrayBuffer(receiver) { - return getBinaryPromise().then(function(binary) { - return WebAssembly.instantiate(binary, info); - }).then(receiver, function(reason) { - err('failed to asynchronously prepare wasm: ' + reason); - - - abort(reason); - }); - } - - // Prefer streaming instantiation if available. - function instantiateAsync() { - if (!wasmBinary && - typeof WebAssembly.instantiateStreaming === 'function' && - !isDataURI(wasmBinaryFile) && - // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously. - !isFileURI(wasmBinaryFile) && - typeof fetch === 'function') { - fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) { - var result = WebAssembly.instantiateStreaming(response, info); - return result.then(receiveInstantiatedSource, function(reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err('wasm streaming compile failed: ' + reason); - err('falling back to ArrayBuffer instantiation'); - return instantiateArrayBuffer(receiveInstantiatedSource); - }); - }); - } else { - return instantiateArrayBuffer(receiveInstantiatedSource); - } - } - // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback - // to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel - // to any other async startup actions they are performing. - if (Module['instantiateWasm']) { - try { - var exports = Module['instantiateWasm'](info, receiveInstance); - return exports; - } catch(e) { - err('Module.instantiateWasm callback failed with error: ' + e); - return false; - } - } - - instantiateAsync(); - return {}; // no exports yet; we'll fill them in later -} - - -// Globals used by JS i64 conversions -var tempDouble; -var tempI64; - -// === Body === - -var ASM_CONSTS = { - -}; - - - - -// STATICTOP = STATIC_BASE + 13312; -/* global initializers */ __ATINIT__.push({ func: function() { ___wasm_call_ctors() } }); - - - - -/* no memory initializer */ -// {{PRE_LIBRARY}} - - - function demangle(func) { - return func; - } - - function demangleAll(text) { - var regex = - /\b_Z[\w\d_]+/g; - return text.replace(regex, - function(x) { - var y = demangle(x); - return x === y ? x : (y + ' [' + x + ']'); - }); - } - - function jsStackTrace() { - var err = new Error(); - if (!err.stack) { - // IE10+ special cases: It does have callstack info, but it is only populated if an Error object is thrown, - // so try that as a special-case. - try { - throw new Error(); - } catch(e) { - err = e; - } - if (!err.stack) { - return '(no stack trace available)'; - } - } - return err.stack.toString(); - } - - function stackTrace() { - var js = jsStackTrace(); - if (Module['extraStackTrace']) js += '\n' + Module['extraStackTrace'](); - return demangleAll(js); - } - - function _emscripten_get_sbrk_ptr() { - return 14176; - } - - function _emscripten_memcpy_big(dest, src, num) { - HEAPU8.copyWithin(dest, src, src + num); - } - - - function _emscripten_get_heap_size() { - return HEAPU8.length; - } - - function emscripten_realloc_buffer(size) { - try { - // round size grow request up to wasm page size (fixed 64KB per spec) - wasmMemory.grow((size - buffer.byteLength + 65535) >>> 16); // .grow() takes a delta compared to the previous size - updateGlobalBufferAndViews(wasmMemory.buffer); - return 1 /*success*/; - } catch(e) { - } - }function _emscripten_resize_heap(requestedSize) { - requestedSize = requestedSize >>> 0; - var oldSize = _emscripten_get_heap_size(); - // With pthreads, races can happen (another thread might increase the size in between), so return a failure, and let the caller retry. - - - var PAGE_MULTIPLE = 65536; - - // Memory resize rules: - // 1. When resizing, always produce a resized heap that is at least 16MB (to avoid tiny heap sizes receiving lots of repeated resizes at startup) - // 2. Always increase heap size to at least the requested size, rounded up to next page multiple. - // 3a. If MEMORY_GROWTH_LINEAR_STEP == -1, excessively resize the heap geometrically: increase the heap size according to - // MEMORY_GROWTH_GEOMETRIC_STEP factor (default +20%), - // At most overreserve by MEMORY_GROWTH_GEOMETRIC_CAP bytes (default 96MB). - // 3b. If MEMORY_GROWTH_LINEAR_STEP != -1, excessively resize the heap linearly: increase the heap size by at least MEMORY_GROWTH_LINEAR_STEP bytes. - // 4. Max size for the heap is capped at 2048MB-PAGE_MULTIPLE, or by MAXIMUM_MEMORY, or by ASAN limit, depending on which is smallest - // 5. If we were unable to allocate as much memory, it may be due to over-eager decision to excessively reserve due to (3) above. - // Hence if an allocation fails, cut down on the amount of excess growth, in an attempt to succeed to perform a smaller allocation. - - // A limit was set for how much we can grow. We should not exceed that - // (the wasm binary specifies it, so if we tried, we'd fail anyhow). - var maxHeapSize = 2147483648; - if (requestedSize > maxHeapSize) { - return false; - } - - var minHeapSize = 16777216; - - // Loop through potential heap size increases. If we attempt a too eager reservation that fails, cut down on the - // attempted size and reserve a smaller bump instead. (max 3 times, chosen somewhat arbitrarily) - for(var cutDown = 1; cutDown <= 4; cutDown *= 2) { - var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); // ensure geometric growth - // but limit overreserving (default to capping at +96MB overgrowth at most) - overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296 ); - - - var newSize = Math.min(maxHeapSize, alignUp(Math.max(minHeapSize, requestedSize, overGrownHeapSize), PAGE_MULTIPLE)); - - var replacement = emscripten_realloc_buffer(newSize); - if (replacement) { - - return true; - } - } - return false; - } - - - - var PATH={splitPath:function(filename) { - var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; - return splitPathRe.exec(filename).slice(1); - },normalizeArray:function(parts, allowAboveRoot) { - // if the path tries to go above the root, `up` ends up > 0 - var up = 0; - for (var i = parts.length - 1; i >= 0; i--) { - var last = parts[i]; - if (last === '.') { - parts.splice(i, 1); - } else if (last === '..') { - parts.splice(i, 1); - up++; - } else if (up) { - parts.splice(i, 1); - up--; - } - } - // if the path is allowed to go above the root, restore leading ..s - if (allowAboveRoot) { - for (; up; up--) { - parts.unshift('..'); - } - } - return parts; - },normalize:function(path) { - var isAbsolute = path.charAt(0) === '/', - trailingSlash = path.substr(-1) === '/'; - // Normalize the path - path = PATH.normalizeArray(path.split('/').filter(function(p) { - return !!p; - }), !isAbsolute).join('/'); - if (!path && !isAbsolute) { - path = '.'; - } - if (path && trailingSlash) { - path += '/'; - } - return (isAbsolute ? '/' : '') + path; - },dirname:function(path) { - var result = PATH.splitPath(path), - root = result[0], - dir = result[1]; - if (!root && !dir) { - // No dirname whatsoever - return '.'; - } - if (dir) { - // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); - } - return root + dir; - },basename:function(path) { - // EMSCRIPTEN return '/'' for '/', not an empty string - if (path === '/') return '/'; - var lastSlash = path.lastIndexOf('/'); - if (lastSlash === -1) return path; - return path.substr(lastSlash+1); - },extname:function(path) { - return PATH.splitPath(path)[3]; - },join:function() { - var paths = Array.prototype.slice.call(arguments, 0); - return PATH.normalize(paths.join('/')); - },join2:function(l, r) { - return PATH.normalize(l + '/' + r); - }}; - - - function setErrNo(value) { - HEAP32[((___errno_location())>>2)]=value; - return value; - } - - var PATH_FS={resolve:function() { - var resolvedPath = '', - resolvedAbsolute = false; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : FS.cwd(); - // Skip empty and invalid entries - if (typeof path !== 'string') { - throw new TypeError('Arguments to path.resolve must be strings'); - } else if (!path) { - return ''; // an invalid portion invalidates the whole thing - } - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = path.charAt(0) === '/'; - } - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) - resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter(function(p) { - return !!p; - }), !resolvedAbsolute).join('/'); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; - },relative:function(from, to) { - from = PATH_FS.resolve(from).substr(1); - to = PATH_FS.resolve(to).substr(1); - function trim(arr) { - var start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') break; - } - var end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') break; - } - if (start > end) return []; - return arr.slice(start, end - start + 1); - } - var fromParts = trim(from.split('/')); - var toParts = trim(to.split('/')); - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; - } - } - var outputParts = []; - for (var i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); - } - outputParts = outputParts.concat(toParts.slice(samePartsLength)); - return outputParts.join('/'); - }}; - - var TTY={ttys:[],init:function () { - // https://github.com/emscripten-core/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // currently, FS.init does not distinguish if process.stdin is a file or TTY - // // device, it always assumes it's a TTY device. because of this, we're forcing - // // process.stdin to UTF8 encoding to at least make stdin reading compatible - // // with text files until FS.init can be refactored. - // process['stdin']['setEncoding']('utf8'); - // } - },shutdown:function() { - // https://github.com/emscripten-core/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // inolen: any idea as to why node -e 'process.stdin.read()' wouldn't exit immediately (with process.stdin being a tty)? - // // isaacs: because now it's reading from the stream, you've expressed interest in it, so that read() kicks off a _read() which creates a ReadReq operation - // // inolen: I thought read() in that case was a synchronous operation that just grabbed some amount of buffered data if it exists? - // // isaacs: it is. but it also triggers a _read() call, which calls readStart() on the handle - // // isaacs: do process.stdin.pause() and i'd think it'd probably close the pending call - // process['stdin']['pause'](); - // } - },register:function(dev, ops) { - TTY.ttys[dev] = { input: [], output: [], ops: ops }; - FS.registerDevice(dev, TTY.stream_ops); - },stream_ops:{open:function(stream) { - var tty = TTY.ttys[stream.node.rdev]; - if (!tty) { - throw new FS.ErrnoError(43); - } - stream.tty = tty; - stream.seekable = false; - },close:function(stream) { - // flush any pending line data - stream.tty.ops.flush(stream.tty); - },flush:function(stream) { - stream.tty.ops.flush(stream.tty); - },read:function(stream, buffer, offset, length, pos /* ignored */) { - if (!stream.tty || !stream.tty.ops.get_char) { - throw new FS.ErrnoError(60); - } - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = stream.tty.ops.get_char(stream.tty); - } catch (e) { - throw new FS.ErrnoError(29); - } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError(6); - } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset+i] = result; - } - if (bytesRead) { - stream.node.timestamp = Date.now(); - } - return bytesRead; - },write:function(stream, buffer, offset, length, pos) { - if (!stream.tty || !stream.tty.ops.put_char) { - throw new FS.ErrnoError(60); - } - try { - for (var i = 0; i < length; i++) { - stream.tty.ops.put_char(stream.tty, buffer[offset+i]); - } - } catch (e) { - throw new FS.ErrnoError(29); - } - if (length) { - stream.node.timestamp = Date.now(); - } - return i; - }},default_tty_ops:{get_char:function(tty) { - if (!tty.input.length) { - var result = null; - if (ENVIRONMENT_IS_NODE) { - // we will read data by chunks of BUFSIZE - var BUFSIZE = 256; - var buf = Buffer.alloc ? Buffer.alloc(BUFSIZE) : new Buffer(BUFSIZE); - var bytesRead = 0; - - try { - bytesRead = nodeFS.readSync(process.stdin.fd, buf, 0, BUFSIZE, null); - } catch(e) { - // Cross-platform differences: on Windows, reading EOF throws an exception, but on other OSes, - // reading EOF returns 0. Uniformize behavior by treating the EOF exception to return 0. - if (e.toString().indexOf('EOF') != -1) bytesRead = 0; - else throw e; - } - - if (bytesRead > 0) { - result = buf.slice(0, bytesRead).toString('utf-8'); - } else { - result = null; - } - } else - if (typeof window != 'undefined' && - typeof window.prompt == 'function') { - // Browser. - result = window.prompt('Input: '); // returns null on cancel - if (result !== null) { - result += '\n'; - } - } else if (typeof readline == 'function') { - // Command line. - result = readline(); - if (result !== null) { - result += '\n'; - } - } - if (!result) { - return null; - } - tty.input = intArrayFromString(result, true); - } - return tty.input.shift(); - },put_char:function(tty, val) { - if (val === null || val === 10) { - out(UTF8ArrayToString(tty.output, 0)); - tty.output = []; - } else { - if (val != 0) tty.output.push(val); // val == 0 would cut text output off in the middle. - } - },flush:function(tty) { - if (tty.output && tty.output.length > 0) { - out(UTF8ArrayToString(tty.output, 0)); - tty.output = []; - } - }},default_tty1_ops:{put_char:function(tty, val) { - if (val === null || val === 10) { - err(UTF8ArrayToString(tty.output, 0)); - tty.output = []; - } else { - if (val != 0) tty.output.push(val); - } - },flush:function(tty) { - if (tty.output && tty.output.length > 0) { - err(UTF8ArrayToString(tty.output, 0)); - tty.output = []; - } - }}}; - - var MEMFS={ops_table:null,mount:function(mount) { - return MEMFS.createNode(null, '/', 16384 | 511 /* 0777 */, 0); - },createNode:function(parent, name, mode, dev) { - if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { - // no supported - throw new FS.ErrnoError(63); - } - if (!MEMFS.ops_table) { - MEMFS.ops_table = { - dir: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - lookup: MEMFS.node_ops.lookup, - mknod: MEMFS.node_ops.mknod, - rename: MEMFS.node_ops.rename, - unlink: MEMFS.node_ops.unlink, - rmdir: MEMFS.node_ops.rmdir, - readdir: MEMFS.node_ops.readdir, - symlink: MEMFS.node_ops.symlink - }, - stream: { - llseek: MEMFS.stream_ops.llseek - } - }, - file: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: { - llseek: MEMFS.stream_ops.llseek, - read: MEMFS.stream_ops.read, - write: MEMFS.stream_ops.write, - allocate: MEMFS.stream_ops.allocate, - mmap: MEMFS.stream_ops.mmap, - msync: MEMFS.stream_ops.msync - } - }, - link: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - readlink: MEMFS.node_ops.readlink - }, - stream: {} - }, - chrdev: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: FS.chrdev_stream_ops - } - }; - } - var node = FS.createNode(parent, name, mode, dev); - if (FS.isDir(node.mode)) { - node.node_ops = MEMFS.ops_table.dir.node; - node.stream_ops = MEMFS.ops_table.dir.stream; - node.contents = {}; - } else if (FS.isFile(node.mode)) { - node.node_ops = MEMFS.ops_table.file.node; - node.stream_ops = MEMFS.ops_table.file.stream; - node.usedBytes = 0; // The actual number of bytes used in the typed array, as opposed to contents.length which gives the whole capacity. - // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred - // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size - // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. - node.contents = null; - } else if (FS.isLink(node.mode)) { - node.node_ops = MEMFS.ops_table.link.node; - node.stream_ops = MEMFS.ops_table.link.stream; - } else if (FS.isChrdev(node.mode)) { - node.node_ops = MEMFS.ops_table.chrdev.node; - node.stream_ops = MEMFS.ops_table.chrdev.stream; - } - node.timestamp = Date.now(); - // add the new node to the parent - if (parent) { - parent.contents[name] = node; - } - return node; - },getFileDataAsRegularArray:function(node) { - if (node.contents && node.contents.subarray) { - var arr = []; - for (var i = 0; i < node.usedBytes; ++i) arr.push(node.contents[i]); - return arr; // Returns a copy of the original data. - } - return node.contents; // No-op, the file contents are already in a JS array. Return as-is. - },getFileDataAsTypedArray:function(node) { - if (!node.contents) return new Uint8Array(0); - if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. - return new Uint8Array(node.contents); - },expandFileStorage:function(node, newCapacity) { - var prevCapacity = node.contents ? node.contents.length : 0; - if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. - // Don't expand strictly to the given requested limit if it's only a very small increase, but instead geometrically grow capacity. - // For small filesizes (<1MB), perform size*2 geometric increase, but for large sizes, do a much more conservative size*1.125 increase to - // avoid overshooting the allocation cap by a very large margin. - var CAPACITY_DOUBLING_MAX = 1024 * 1024; - newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); - if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. - var oldContents = node.contents; - node.contents = new Uint8Array(newCapacity); // Allocate new storage. - if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); // Copy old data over to the new storage. - return; - },resizeFileStorage:function(node, newSize) { - if (node.usedBytes == newSize) return; - if (newSize == 0) { - node.contents = null; // Fully decommit when requesting a resize to zero. - node.usedBytes = 0; - return; - } - if (!node.contents || node.contents.subarray) { // Resize a typed array if that is being used as the backing store. - var oldContents = node.contents; - node.contents = new Uint8Array(newSize); // Allocate new storage. - if (oldContents) { - node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. - } - node.usedBytes = newSize; - return; - } - // Backing with a JS array. - if (!node.contents) node.contents = []; - if (node.contents.length > newSize) node.contents.length = newSize; - else while (node.contents.length < newSize) node.contents.push(0); - node.usedBytes = newSize; - },node_ops:{getattr:function(node) { - var attr = {}; - // device numbers reuse inode numbers. - attr.dev = FS.isChrdev(node.mode) ? node.id : 1; - attr.ino = node.id; - attr.mode = node.mode; - attr.nlink = 1; - attr.uid = 0; - attr.gid = 0; - attr.rdev = node.rdev; - if (FS.isDir(node.mode)) { - attr.size = 4096; - } else if (FS.isFile(node.mode)) { - attr.size = node.usedBytes; - } else if (FS.isLink(node.mode)) { - attr.size = node.link.length; - } else { - attr.size = 0; - } - attr.atime = new Date(node.timestamp); - attr.mtime = new Date(node.timestamp); - attr.ctime = new Date(node.timestamp); - // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), - // but this is not required by the standard. - attr.blksize = 4096; - attr.blocks = Math.ceil(attr.size / attr.blksize); - return attr; - },setattr:function(node, attr) { - if (attr.mode !== undefined) { - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - node.timestamp = attr.timestamp; - } - if (attr.size !== undefined) { - MEMFS.resizeFileStorage(node, attr.size); - } - },lookup:function(parent, name) { - throw FS.genericErrors[44]; - },mknod:function(parent, name, mode, dev) { - return MEMFS.createNode(parent, name, mode, dev); - },rename:function(old_node, new_dir, new_name) { - // if we're overwriting a directory at new_name, make sure it's empty. - if (FS.isDir(old_node.mode)) { - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) { - } - if (new_node) { - for (var i in new_node.contents) { - throw new FS.ErrnoError(55); - } - } - } - // do the internal rewiring - delete old_node.parent.contents[old_node.name]; - old_node.name = new_name; - new_dir.contents[new_name] = old_node; - old_node.parent = new_dir; - },unlink:function(parent, name) { - delete parent.contents[name]; - },rmdir:function(parent, name) { - var node = FS.lookupNode(parent, name); - for (var i in node.contents) { - throw new FS.ErrnoError(55); - } - delete parent.contents[name]; - },readdir:function(node) { - var entries = ['.', '..']; - for (var key in node.contents) { - if (!node.contents.hasOwnProperty(key)) { - continue; - } - entries.push(key); - } - return entries; - },symlink:function(parent, newname, oldpath) { - var node = MEMFS.createNode(parent, newname, 511 /* 0777 */ | 40960, 0); - node.link = oldpath; - return node; - },readlink:function(node) { - if (!FS.isLink(node.mode)) { - throw new FS.ErrnoError(28); - } - return node.link; - }},stream_ops:{read:function(stream, buffer, offset, length, position) { - var contents = stream.node.contents; - if (position >= stream.node.usedBytes) return 0; - var size = Math.min(stream.node.usedBytes - position, length); - if (size > 8 && contents.subarray) { // non-trivial, and typed array - buffer.set(contents.subarray(position, position + size), offset); - } else { - for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i]; - } - return size; - },write:function(stream, buffer, offset, length, position, canOwn) { - // If the buffer is located in main memory (HEAP), and if - // memory can grow, we can't hold on to references of the - // memory buffer, as they may get invalidated. That means we - // need to do copy its contents. - if (buffer.buffer === HEAP8.buffer) { - canOwn = false; - } - - if (!length) return 0; - var node = stream.node; - node.timestamp = Date.now(); - - if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array? - if (canOwn) { - node.contents = buffer.subarray(offset, offset + length); - node.usedBytes = length; - return length; - } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. - node.contents = buffer.slice(offset, offset + length); - node.usedBytes = length; - return length; - } else if (position + length <= node.usedBytes) { // Writing to an already allocated and used subrange of the file? - node.contents.set(buffer.subarray(offset, offset + length), position); - return length; - } - } - - // Appending to an existing file and we need to reallocate, or source data did not come as a typed array. - MEMFS.expandFileStorage(node, position+length); - if (node.contents.subarray && buffer.subarray) node.contents.set(buffer.subarray(offset, offset + length), position); // Use typed array write if available. - else { - for (var i = 0; i < length; i++) { - node.contents[position + i] = buffer[offset + i]; // Or fall back to manual write if not. - } - } - node.usedBytes = Math.max(node.usedBytes, position + length); - return length; - },llseek:function(stream, offset, whence) { - var position = offset; - if (whence === 1) { - position += stream.position; - } else if (whence === 2) { - if (FS.isFile(stream.node.mode)) { - position += stream.node.usedBytes; - } - } - if (position < 0) { - throw new FS.ErrnoError(28); - } - return position; - },allocate:function(stream, offset, length) { - MEMFS.expandFileStorage(stream.node, offset + length); - stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length); - },mmap:function(stream, address, length, position, prot, flags) { - // We don't currently support location hints for the address of the mapping - assert(address === 0); - - if (!FS.isFile(stream.node.mode)) { - throw new FS.ErrnoError(43); - } - var ptr; - var allocated; - var contents = stream.node.contents; - // Only make a new copy when MAP_PRIVATE is specified. - if (!(flags & 2) && contents.buffer === buffer) { - // We can't emulate MAP_SHARED when the file is not backed by the buffer - // we're mapping to (e.g. the HEAP buffer). - allocated = false; - ptr = contents.byteOffset; - } else { - // Try to avoid unnecessary slices. - if (position > 0 || position + length < contents.length) { - if (contents.subarray) { - contents = contents.subarray(position, position + length); - } else { - contents = Array.prototype.slice.call(contents, position, position + length); - } - } - allocated = true; - ptr = _malloc(length); - if (!ptr) { - throw new FS.ErrnoError(48); - } - HEAP8.set(contents, ptr); - } - return { ptr: ptr, allocated: allocated }; - },msync:function(stream, buffer, offset, length, mmapFlags) { - if (!FS.isFile(stream.node.mode)) { - throw new FS.ErrnoError(43); - } - if (mmapFlags & 2) { - // MAP_PRIVATE calls need not to be synced back to underlying fs - return 0; - } - - var bytesWritten = MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); - // should we check if bytesWritten and length are the same? - return 0; - }}};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,trackingDelegate:{},tracking:{openFlags:{READ:1,WRITE:2}},ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,handleFSError:function(e) { - if (!(e instanceof FS.ErrnoError)) throw e + ' : ' + stackTrace(); - return setErrNo(e.errno); - },lookupPath:function(path, opts) { - path = PATH_FS.resolve(FS.cwd(), path); - opts = opts || {}; - - if (!path) return { path: '', node: null }; - - var defaults = { - follow_mount: true, - recurse_count: 0 - }; - for (var key in defaults) { - if (opts[key] === undefined) { - opts[key] = defaults[key]; - } - } - - if (opts.recurse_count > 8) { // max recursive lookup of 8 - throw new FS.ErrnoError(32); - } - - // split the path - var parts = PATH.normalizeArray(path.split('/').filter(function(p) { - return !!p; - }), false); - - // start at the root - var current = FS.root; - var current_path = '/'; - - for (var i = 0; i < parts.length; i++) { - var islast = (i === parts.length-1); - if (islast && opts.parent) { - // stop resolving - break; - } - - current = FS.lookupNode(current, parts[i]); - current_path = PATH.join2(current_path, parts[i]); - - // jump to the mount's root node if this is a mountpoint - if (FS.isMountpoint(current)) { - if (!islast || (islast && opts.follow_mount)) { - current = current.mounted.root; - } - } - - // by default, lookupPath will not follow a symlink if it is the final path component. - // setting opts.follow = true will override this behavior. - if (!islast || opts.follow) { - var count = 0; - while (FS.isLink(current.mode)) { - var link = FS.readlink(current_path); - current_path = PATH_FS.resolve(PATH.dirname(current_path), link); - - var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count }); - current = lookup.node; - - if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). - throw new FS.ErrnoError(32); - } - } - } - } - - return { path: current_path, node: current }; - },getPath:function(node) { - var path; - while (true) { - if (FS.isRoot(node)) { - var mount = node.mount.mountpoint; - if (!path) return mount; - return mount[mount.length-1] !== '/' ? mount + '/' + path : mount + path; - } - path = path ? node.name + '/' + path : node.name; - node = node.parent; - } - },hashName:function(parentid, name) { - var hash = 0; - - - for (var i = 0; i < name.length; i++) { - hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; - } - return ((parentid + hash) >>> 0) % FS.nameTable.length; - },hashAddNode:function(node) { - var hash = FS.hashName(node.parent.id, node.name); - node.name_next = FS.nameTable[hash]; - FS.nameTable[hash] = node; - },hashRemoveNode:function(node) { - var hash = FS.hashName(node.parent.id, node.name); - if (FS.nameTable[hash] === node) { - FS.nameTable[hash] = node.name_next; - } else { - var current = FS.nameTable[hash]; - while (current) { - if (current.name_next === node) { - current.name_next = node.name_next; - break; - } - current = current.name_next; - } - } - },lookupNode:function(parent, name) { - var errCode = FS.mayLookup(parent); - if (errCode) { - throw new FS.ErrnoError(errCode, parent); - } - var hash = FS.hashName(parent.id, name); - for (var node = FS.nameTable[hash]; node; node = node.name_next) { - var nodeName = node.name; - if (node.parent.id === parent.id && nodeName === name) { - return node; - } - } - // if we failed to find it in the cache, call into the VFS - return FS.lookup(parent, name); - },createNode:function(parent, name, mode, rdev) { - var node = new FS.FSNode(parent, name, mode, rdev); - - FS.hashAddNode(node); - - return node; - },destroyNode:function(node) { - FS.hashRemoveNode(node); - },isRoot:function(node) { - return node === node.parent; - },isMountpoint:function(node) { - return !!node.mounted; - },isFile:function(mode) { - return (mode & 61440) === 32768; - },isDir:function(mode) { - return (mode & 61440) === 16384; - },isLink:function(mode) { - return (mode & 61440) === 40960; - },isChrdev:function(mode) { - return (mode & 61440) === 8192; - },isBlkdev:function(mode) { - return (mode & 61440) === 24576; - },isFIFO:function(mode) { - return (mode & 61440) === 4096; - },isSocket:function(mode) { - return (mode & 49152) === 49152; - },flagModes:{"r":0,"rs":1052672,"r+":2,"w":577,"wx":705,"xw":705,"w+":578,"wx+":706,"xw+":706,"a":1089,"ax":1217,"xa":1217,"a+":1090,"ax+":1218,"xa+":1218},modeStringToFlags:function(str) { - var flags = FS.flagModes[str]; - if (typeof flags === 'undefined') { - throw new Error('Unknown file open mode: ' + str); - } - return flags; - },flagsToPermissionString:function(flag) { - var perms = ['r', 'w', 'rw'][flag & 3]; - if ((flag & 512)) { - perms += 'w'; - } - return perms; - },nodePermissions:function(node, perms) { - if (FS.ignorePermissions) { - return 0; - } - // return 0 if any user, group or owner bits are set. - if (perms.indexOf('r') !== -1 && !(node.mode & 292)) { - return 2; - } else if (perms.indexOf('w') !== -1 && !(node.mode & 146)) { - return 2; - } else if (perms.indexOf('x') !== -1 && !(node.mode & 73)) { - return 2; - } - return 0; - },mayLookup:function(dir) { - var errCode = FS.nodePermissions(dir, 'x'); - if (errCode) return errCode; - if (!dir.node_ops.lookup) return 2; - return 0; - },mayCreate:function(dir, name) { - try { - var node = FS.lookupNode(dir, name); - return 20; - } catch (e) { - } - return FS.nodePermissions(dir, 'wx'); - },mayDelete:function(dir, name, isdir) { - var node; - try { - node = FS.lookupNode(dir, name); - } catch (e) { - return e.errno; - } - var errCode = FS.nodePermissions(dir, 'wx'); - if (errCode) { - return errCode; - } - if (isdir) { - if (!FS.isDir(node.mode)) { - return 54; - } - if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { - return 10; - } - } else { - if (FS.isDir(node.mode)) { - return 31; - } - } - return 0; - },mayOpen:function(node, flags) { - if (!node) { - return 44; - } - if (FS.isLink(node.mode)) { - return 32; - } else if (FS.isDir(node.mode)) { - if (FS.flagsToPermissionString(flags) !== 'r' || // opening for write - (flags & 512)) { // TODO: check for O_SEARCH? (== search for dir only) - return 31; - } - } - return FS.nodePermissions(node, FS.flagsToPermissionString(flags)); - },MAX_OPEN_FDS:4096,nextfd:function(fd_start, fd_end) { - fd_start = fd_start || 0; - fd_end = fd_end || FS.MAX_OPEN_FDS; - for (var fd = fd_start; fd <= fd_end; fd++) { - if (!FS.streams[fd]) { - return fd; - } - } - throw new FS.ErrnoError(33); - },getStream:function(fd) { - return FS.streams[fd]; - },createStream:function(stream, fd_start, fd_end) { - if (!FS.FSStream) { - FS.FSStream = /** @constructor */ function(){}; - FS.FSStream.prototype = { - object: { - get: function() { return this.node; }, - set: function(val) { this.node = val; } - }, - isRead: { - get: function() { return (this.flags & 2097155) !== 1; } - }, - isWrite: { - get: function() { return (this.flags & 2097155) !== 0; } - }, - isAppend: { - get: function() { return (this.flags & 1024); } - } - }; - } - // clone it, so we can return an instance of FSStream - var newStream = new FS.FSStream(); - for (var p in stream) { - newStream[p] = stream[p]; - } - stream = newStream; - var fd = FS.nextfd(fd_start, fd_end); - stream.fd = fd; - FS.streams[fd] = stream; - return stream; - },closeStream:function(fd) { - FS.streams[fd] = null; - },chrdev_stream_ops:{open:function(stream) { - var device = FS.getDevice(stream.node.rdev); - // override node's stream ops with the device's - stream.stream_ops = device.stream_ops; - // forward the open call - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } - },llseek:function() { - throw new FS.ErrnoError(70); - }},major:function(dev) { - return ((dev) >> 8); - },minor:function(dev) { - return ((dev) & 0xff); - },makedev:function(ma, mi) { - return ((ma) << 8 | (mi)); - },registerDevice:function(dev, ops) { - FS.devices[dev] = { stream_ops: ops }; - },getDevice:function(dev) { - return FS.devices[dev]; - },getMounts:function(mount) { - var mounts = []; - var check = [mount]; - - while (check.length) { - var m = check.pop(); - - mounts.push(m); - - check.push.apply(check, m.mounts); - } - - return mounts; - },syncfs:function(populate, callback) { - if (typeof(populate) === 'function') { - callback = populate; - populate = false; - } - - FS.syncFSRequests++; - - if (FS.syncFSRequests > 1) { - err('warning: ' + FS.syncFSRequests + ' FS.syncfs operations in flight at once, probably just doing extra work'); - } - - var mounts = FS.getMounts(FS.root.mount); - var completed = 0; - - function doCallback(errCode) { - FS.syncFSRequests--; - return callback(errCode); - } - - function done(errCode) { - if (errCode) { - if (!done.errored) { - done.errored = true; - return doCallback(errCode); - } - return; - } - if (++completed >= mounts.length) { - doCallback(null); - } - }; - - // sync all mounts - mounts.forEach(function (mount) { - if (!mount.type.syncfs) { - return done(null); - } - mount.type.syncfs(mount, populate, done); - }); - },mount:function(type, opts, mountpoint) { - var root = mountpoint === '/'; - var pseudo = !mountpoint; - var node; - - if (root && FS.root) { - throw new FS.ErrnoError(10); - } else if (!root && !pseudo) { - var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - - mountpoint = lookup.path; // use the absolute path - node = lookup.node; - - if (FS.isMountpoint(node)) { - throw new FS.ErrnoError(10); - } - - if (!FS.isDir(node.mode)) { - throw new FS.ErrnoError(54); - } - } - - var mount = { - type: type, - opts: opts, - mountpoint: mountpoint, - mounts: [] - }; - - // create a root node for the fs - var mountRoot = type.mount(mount); - mountRoot.mount = mount; - mount.root = mountRoot; - - if (root) { - FS.root = mountRoot; - } else if (node) { - // set as a mountpoint - node.mounted = mount; - - // add the new mount to the current mount's children - if (node.mount) { - node.mount.mounts.push(mount); - } - } - - return mountRoot; - },unmount:function (mountpoint) { - var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - - if (!FS.isMountpoint(lookup.node)) { - throw new FS.ErrnoError(28); - } - - // destroy the nodes for this mount, and all its child mounts - var node = lookup.node; - var mount = node.mounted; - var mounts = FS.getMounts(mount); - - Object.keys(FS.nameTable).forEach(function (hash) { - var current = FS.nameTable[hash]; - - while (current) { - var next = current.name_next; - - if (mounts.indexOf(current.mount) !== -1) { - FS.destroyNode(current); - } - - current = next; - } - }); - - // no longer a mountpoint - node.mounted = null; - - // remove this mount from the child mounts - var idx = node.mount.mounts.indexOf(mount); - node.mount.mounts.splice(idx, 1); - },lookup:function(parent, name) { - return parent.node_ops.lookup(parent, name); - },mknod:function(path, mode, dev) { - var lookup = FS.lookupPath(path, { parent: true }); - var parent = lookup.node; - var name = PATH.basename(path); - if (!name || name === '.' || name === '..') { - throw new FS.ErrnoError(28); - } - var errCode = FS.mayCreate(parent, name); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - if (!parent.node_ops.mknod) { - throw new FS.ErrnoError(63); - } - return parent.node_ops.mknod(parent, name, mode, dev); - },create:function(path, mode) { - mode = mode !== undefined ? mode : 438 /* 0666 */; - mode &= 4095; - mode |= 32768; - return FS.mknod(path, mode, 0); - },mkdir:function(path, mode) { - mode = mode !== undefined ? mode : 511 /* 0777 */; - mode &= 511 | 512; - mode |= 16384; - return FS.mknod(path, mode, 0); - },mkdirTree:function(path, mode) { - var dirs = path.split('/'); - var d = ''; - for (var i = 0; i < dirs.length; ++i) { - if (!dirs[i]) continue; - d += '/' + dirs[i]; - try { - FS.mkdir(d, mode); - } catch(e) { - if (e.errno != 20) throw e; - } - } - },mkdev:function(path, mode, dev) { - if (typeof(dev) === 'undefined') { - dev = mode; - mode = 438 /* 0666 */; - } - mode |= 8192; - return FS.mknod(path, mode, dev); - },symlink:function(oldpath, newpath) { - if (!PATH_FS.resolve(oldpath)) { - throw new FS.ErrnoError(44); - } - var lookup = FS.lookupPath(newpath, { parent: true }); - var parent = lookup.node; - if (!parent) { - throw new FS.ErrnoError(44); - } - var newname = PATH.basename(newpath); - var errCode = FS.mayCreate(parent, newname); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - if (!parent.node_ops.symlink) { - throw new FS.ErrnoError(63); - } - return parent.node_ops.symlink(parent, newname, oldpath); - },rename:function(old_path, new_path) { - var old_dirname = PATH.dirname(old_path); - var new_dirname = PATH.dirname(new_path); - var old_name = PATH.basename(old_path); - var new_name = PATH.basename(new_path); - // parents must exist - var lookup, old_dir, new_dir; - try { - lookup = FS.lookupPath(old_path, { parent: true }); - old_dir = lookup.node; - lookup = FS.lookupPath(new_path, { parent: true }); - new_dir = lookup.node; - } catch (e) { - throw new FS.ErrnoError(10); - } - if (!old_dir || !new_dir) throw new FS.ErrnoError(44); - // need to be part of the same mount - if (old_dir.mount !== new_dir.mount) { - throw new FS.ErrnoError(75); - } - // source must exist - var old_node = FS.lookupNode(old_dir, old_name); - // old path should not be an ancestor of the new path - var relative = PATH_FS.relative(old_path, new_dirname); - if (relative.charAt(0) !== '.') { - throw new FS.ErrnoError(28); - } - // new path should not be an ancestor of the old path - relative = PATH_FS.relative(new_path, old_dirname); - if (relative.charAt(0) !== '.') { - throw new FS.ErrnoError(55); - } - // see if the new path already exists - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) { - // not fatal - } - // early out if nothing needs to change - if (old_node === new_node) { - return; - } - // we'll need to delete the old entry - var isdir = FS.isDir(old_node.mode); - var errCode = FS.mayDelete(old_dir, old_name, isdir); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - // need delete permissions if we'll be overwriting. - // need create permissions if new doesn't already exist. - errCode = new_node ? - FS.mayDelete(new_dir, new_name, isdir) : - FS.mayCreate(new_dir, new_name); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - if (!old_dir.node_ops.rename) { - throw new FS.ErrnoError(63); - } - if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { - throw new FS.ErrnoError(10); - } - // if we are going to change the parent, check write permissions - if (new_dir !== old_dir) { - errCode = FS.nodePermissions(old_dir, 'w'); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - } - try { - if (FS.trackingDelegate['willMovePath']) { - FS.trackingDelegate['willMovePath'](old_path, new_path); - } - } catch(e) { - err("FS.trackingDelegate['willMovePath']('"+old_path+"', '"+new_path+"') threw an exception: " + e.message); - } - // remove the node from the lookup hash - FS.hashRemoveNode(old_node); - // do the underlying fs rename - try { - old_dir.node_ops.rename(old_node, new_dir, new_name); - } catch (e) { - throw e; - } finally { - // add the node back to the hash (in case node_ops.rename - // changed its name) - FS.hashAddNode(old_node); - } - try { - if (FS.trackingDelegate['onMovePath']) FS.trackingDelegate['onMovePath'](old_path, new_path); - } catch(e) { - err("FS.trackingDelegate['onMovePath']('"+old_path+"', '"+new_path+"') threw an exception: " + e.message); - } - },rmdir:function(path) { - var lookup = FS.lookupPath(path, { parent: true }); - var parent = lookup.node; - var name = PATH.basename(path); - var node = FS.lookupNode(parent, name); - var errCode = FS.mayDelete(parent, name, true); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - if (!parent.node_ops.rmdir) { - throw new FS.ErrnoError(63); - } - if (FS.isMountpoint(node)) { - throw new FS.ErrnoError(10); - } - try { - if (FS.trackingDelegate['willDeletePath']) { - FS.trackingDelegate['willDeletePath'](path); - } - } catch(e) { - err("FS.trackingDelegate['willDeletePath']('"+path+"') threw an exception: " + e.message); - } - parent.node_ops.rmdir(parent, name); - FS.destroyNode(node); - try { - if (FS.trackingDelegate['onDeletePath']) FS.trackingDelegate['onDeletePath'](path); - } catch(e) { - err("FS.trackingDelegate['onDeletePath']('"+path+"') threw an exception: " + e.message); - } - },readdir:function(path) { - var lookup = FS.lookupPath(path, { follow: true }); - var node = lookup.node; - if (!node.node_ops.readdir) { - throw new FS.ErrnoError(54); - } - return node.node_ops.readdir(node); - },unlink:function(path) { - var lookup = FS.lookupPath(path, { parent: true }); - var parent = lookup.node; - var name = PATH.basename(path); - var node = FS.lookupNode(parent, name); - var errCode = FS.mayDelete(parent, name, false); - if (errCode) { - // According to POSIX, we should map EISDIR to EPERM, but - // we instead do what Linux does (and we must, as we use - // the musl linux libc). - throw new FS.ErrnoError(errCode); - } - if (!parent.node_ops.unlink) { - throw new FS.ErrnoError(63); - } - if (FS.isMountpoint(node)) { - throw new FS.ErrnoError(10); - } - try { - if (FS.trackingDelegate['willDeletePath']) { - FS.trackingDelegate['willDeletePath'](path); - } - } catch(e) { - err("FS.trackingDelegate['willDeletePath']('"+path+"') threw an exception: " + e.message); - } - parent.node_ops.unlink(parent, name); - FS.destroyNode(node); - try { - if (FS.trackingDelegate['onDeletePath']) FS.trackingDelegate['onDeletePath'](path); - } catch(e) { - err("FS.trackingDelegate['onDeletePath']('"+path+"') threw an exception: " + e.message); - } - },readlink:function(path) { - var lookup = FS.lookupPath(path); - var link = lookup.node; - if (!link) { - throw new FS.ErrnoError(44); - } - if (!link.node_ops.readlink) { - throw new FS.ErrnoError(28); - } - return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)); - },stat:function(path, dontFollow) { - var lookup = FS.lookupPath(path, { follow: !dontFollow }); - var node = lookup.node; - if (!node) { - throw new FS.ErrnoError(44); - } - if (!node.node_ops.getattr) { - throw new FS.ErrnoError(63); - } - return node.node_ops.getattr(node); - },lstat:function(path) { - return FS.stat(path, true); - },chmod:function(path, mode, dontFollow) { - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { follow: !dontFollow }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - mode: (mode & 4095) | (node.mode & ~4095), - timestamp: Date.now() - }); - },lchmod:function(path, mode) { - FS.chmod(path, mode, true); - },fchmod:function(fd, mode) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(8); - } - FS.chmod(stream.node, mode); - },chown:function(path, uid, gid, dontFollow) { - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { follow: !dontFollow }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - timestamp: Date.now() - // we ignore the uid / gid for now - }); - },lchown:function(path, uid, gid) { - FS.chown(path, uid, gid, true); - },fchown:function(fd, uid, gid) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(8); - } - FS.chown(stream.node, uid, gid); - },truncate:function(path, len) { - if (len < 0) { - throw new FS.ErrnoError(28); - } - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { follow: true }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - if (FS.isDir(node.mode)) { - throw new FS.ErrnoError(31); - } - if (!FS.isFile(node.mode)) { - throw new FS.ErrnoError(28); - } - var errCode = FS.nodePermissions(node, 'w'); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - node.node_ops.setattr(node, { - size: len, - timestamp: Date.now() - }); - },ftruncate:function(fd, len) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(8); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(28); - } - FS.truncate(stream.node, len); - },utime:function(path, atime, mtime) { - var lookup = FS.lookupPath(path, { follow: true }); - var node = lookup.node; - node.node_ops.setattr(node, { - timestamp: Math.max(atime, mtime) - }); - },open:function(path, flags, mode, fd_start, fd_end) { - if (path === "") { - throw new FS.ErrnoError(44); - } - flags = typeof flags === 'string' ? FS.modeStringToFlags(flags) : flags; - mode = typeof mode === 'undefined' ? 438 /* 0666 */ : mode; - if ((flags & 64)) { - mode = (mode & 4095) | 32768; - } else { - mode = 0; - } - var node; - if (typeof path === 'object') { - node = path; - } else { - path = PATH.normalize(path); - try { - var lookup = FS.lookupPath(path, { - follow: !(flags & 131072) - }); - node = lookup.node; - } catch (e) { - // ignore - } - } - // perhaps we need to create the node - var created = false; - if ((flags & 64)) { - if (node) { - // if O_CREAT and O_EXCL are set, error out if the node already exists - if ((flags & 128)) { - throw new FS.ErrnoError(20); - } - } else { - // node doesn't exist, try to create it - node = FS.mknod(path, mode, 0); - created = true; - } - } - if (!node) { - throw new FS.ErrnoError(44); - } - // can't truncate a device - if (FS.isChrdev(node.mode)) { - flags &= ~512; - } - // if asked only for a directory, then this must be one - if ((flags & 65536) && !FS.isDir(node.mode)) { - throw new FS.ErrnoError(54); - } - // check permissions, if this is not a file we just created now (it is ok to - // create and write to a file with read-only permissions; it is read-only - // for later use) - if (!created) { - var errCode = FS.mayOpen(node, flags); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - } - // do truncation if necessary - if ((flags & 512)) { - FS.truncate(node, 0); - } - // we've already handled these, don't pass down to the underlying vfs - flags &= ~(128 | 512 | 131072); - - // register the stream with the filesystem - var stream = FS.createStream({ - node: node, - path: FS.getPath(node), // we want the absolute path to the node - flags: flags, - seekable: true, - position: 0, - stream_ops: node.stream_ops, - // used by the file family libc calls (fopen, fwrite, ferror, etc.) - ungotten: [], - error: false - }, fd_start, fd_end); - // call the new stream's open function - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } - if (Module['logReadFiles'] && !(flags & 1)) { - if (!FS.readFiles) FS.readFiles = {}; - if (!(path in FS.readFiles)) { - FS.readFiles[path] = 1; - err("FS.trackingDelegate error on read file: " + path); - } - } - try { - if (FS.trackingDelegate['onOpenFile']) { - var trackingFlags = 0; - if ((flags & 2097155) !== 1) { - trackingFlags |= FS.tracking.openFlags.READ; - } - if ((flags & 2097155) !== 0) { - trackingFlags |= FS.tracking.openFlags.WRITE; - } - FS.trackingDelegate['onOpenFile'](path, trackingFlags); - } - } catch(e) { - err("FS.trackingDelegate['onOpenFile']('"+path+"', flags) threw an exception: " + e.message); - } - return stream; - },close:function(stream) { - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if (stream.getdents) stream.getdents = null; // free readdir state - try { - if (stream.stream_ops.close) { - stream.stream_ops.close(stream); - } - } catch (e) { - throw e; - } finally { - FS.closeStream(stream.fd); - } - stream.fd = null; - },isClosed:function(stream) { - return stream.fd === null; - },llseek:function(stream, offset, whence) { - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if (!stream.seekable || !stream.stream_ops.llseek) { - throw new FS.ErrnoError(70); - } - if (whence != 0 && whence != 1 && whence != 2) { - throw new FS.ErrnoError(28); - } - stream.position = stream.stream_ops.llseek(stream, offset, whence); - stream.ungotten = []; - return stream.position; - },read:function(stream, buffer, offset, length, position) { - if (length < 0 || position < 0) { - throw new FS.ErrnoError(28); - } - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if ((stream.flags & 2097155) === 1) { - throw new FS.ErrnoError(8); - } - if (FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(31); - } - if (!stream.stream_ops.read) { - throw new FS.ErrnoError(28); - } - var seeking = typeof position !== 'undefined'; - if (!seeking) { - position = stream.position; - } else if (!stream.seekable) { - throw new FS.ErrnoError(70); - } - var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); - if (!seeking) stream.position += bytesRead; - return bytesRead; - },write:function(stream, buffer, offset, length, position, canOwn) { - if (length < 0 || position < 0) { - throw new FS.ErrnoError(28); - } - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(8); - } - if (FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(31); - } - if (!stream.stream_ops.write) { - throw new FS.ErrnoError(28); - } - if (stream.seekable && stream.flags & 1024) { - // seek to the end before writing in append mode - FS.llseek(stream, 0, 2); - } - var seeking = typeof position !== 'undefined'; - if (!seeking) { - position = stream.position; - } else if (!stream.seekable) { - throw new FS.ErrnoError(70); - } - var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); - if (!seeking) stream.position += bytesWritten; - try { - if (stream.path && FS.trackingDelegate['onWriteToFile']) FS.trackingDelegate['onWriteToFile'](stream.path); - } catch(e) { - err("FS.trackingDelegate['onWriteToFile']('"+stream.path+"') threw an exception: " + e.message); - } - return bytesWritten; - },allocate:function(stream, offset, length) { - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if (offset < 0 || length <= 0) { - throw new FS.ErrnoError(28); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(8); - } - if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(43); - } - if (!stream.stream_ops.allocate) { - throw new FS.ErrnoError(138); - } - stream.stream_ops.allocate(stream, offset, length); - },mmap:function(stream, address, length, position, prot, flags) { - // User requests writing to file (prot & PROT_WRITE != 0). - // Checking if we have permissions to write to the file unless - // MAP_PRIVATE flag is set. According to POSIX spec it is possible - // to write to file opened in read-only mode with MAP_PRIVATE flag, - // as all modifications will be visible only in the memory of - // the current process. - if ((prot & 2) !== 0 - && (flags & 2) === 0 - && (stream.flags & 2097155) !== 2) { - throw new FS.ErrnoError(2); - } - if ((stream.flags & 2097155) === 1) { - throw new FS.ErrnoError(2); - } - if (!stream.stream_ops.mmap) { - throw new FS.ErrnoError(43); - } - return stream.stream_ops.mmap(stream, address, length, position, prot, flags); - },msync:function(stream, buffer, offset, length, mmapFlags) { - if (!stream || !stream.stream_ops.msync) { - return 0; - } - return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); - },munmap:function(stream) { - return 0; - },ioctl:function(stream, cmd, arg) { - if (!stream.stream_ops.ioctl) { - throw new FS.ErrnoError(59); - } - return stream.stream_ops.ioctl(stream, cmd, arg); - },readFile:function(path, opts) { - opts = opts || {}; - opts.flags = opts.flags || 'r'; - opts.encoding = opts.encoding || 'binary'; - if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { - throw new Error('Invalid encoding type "' + opts.encoding + '"'); - } - var ret; - var stream = FS.open(path, opts.flags); - var stat = FS.stat(path); - var length = stat.size; - var buf = new Uint8Array(length); - FS.read(stream, buf, 0, length, 0); - if (opts.encoding === 'utf8') { - ret = UTF8ArrayToString(buf, 0); - } else if (opts.encoding === 'binary') { - ret = buf; - } - FS.close(stream); - return ret; - },writeFile:function(path, data, opts) { - opts = opts || {}; - opts.flags = opts.flags || 'w'; - var stream = FS.open(path, opts.flags, opts.mode); - if (typeof data === 'string') { - var buf = new Uint8Array(lengthBytesUTF8(data)+1); - var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); - FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn); - } else if (ArrayBuffer.isView(data)) { - FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); - } else { - throw new Error('Unsupported data type'); - } - FS.close(stream); - },cwd:function() { - return FS.currentPath; - },chdir:function(path) { - var lookup = FS.lookupPath(path, { follow: true }); - if (lookup.node === null) { - throw new FS.ErrnoError(44); - } - if (!FS.isDir(lookup.node.mode)) { - throw new FS.ErrnoError(54); - } - var errCode = FS.nodePermissions(lookup.node, 'x'); - if (errCode) { - throw new FS.ErrnoError(errCode); - } - FS.currentPath = lookup.path; - },createDefaultDirectories:function() { - FS.mkdir('/tmp'); - FS.mkdir('/home'); - FS.mkdir('/home/web_user'); - },createDefaultDevices:function() { - // create /dev - FS.mkdir('/dev'); - // setup /dev/null - FS.registerDevice(FS.makedev(1, 3), { - read: function() { return 0; }, - write: function(stream, buffer, offset, length, pos) { return length; } - }); - FS.mkdev('/dev/null', FS.makedev(1, 3)); - // setup /dev/tty and /dev/tty1 - // stderr needs to print output using Module['printErr'] - // so we register a second tty just for it. - TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); - TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); - FS.mkdev('/dev/tty', FS.makedev(5, 0)); - FS.mkdev('/dev/tty1', FS.makedev(6, 0)); - // setup /dev/[u]random - var random_device; - if (typeof crypto === 'object' && typeof crypto['getRandomValues'] === 'function') { - // for modern web browsers - var randomBuffer = new Uint8Array(1); - random_device = function() { crypto.getRandomValues(randomBuffer); return randomBuffer[0]; }; - } else - if (ENVIRONMENT_IS_NODE) { - // for nodejs with or without crypto support included - try { - var crypto_module = require('crypto'); - // nodejs has crypto support - random_device = function() { return crypto_module['randomBytes'](1)[0]; }; - } catch (e) { - // nodejs doesn't have crypto support - } - } else - {} - if (!random_device) { - // we couldn't find a proper implementation, as Math.random() is not suitable for /dev/random, see emscripten-core/emscripten/pull/7096 - random_device = function() { abort("random_device"); }; - } - FS.createDevice('/dev', 'random', random_device); - FS.createDevice('/dev', 'urandom', random_device); - // we're not going to emulate the actual shm device, - // just create the tmp dirs that reside in it commonly - FS.mkdir('/dev/shm'); - FS.mkdir('/dev/shm/tmp'); - },createSpecialDirectories:function() { - // create /proc/self/fd which allows /proc/self/fd/6 => readlink gives the name of the stream for fd 6 (see test_unistd_ttyname) - FS.mkdir('/proc'); - FS.mkdir('/proc/self'); - FS.mkdir('/proc/self/fd'); - FS.mount({ - mount: function() { - var node = FS.createNode('/proc/self', 'fd', 16384 | 511 /* 0777 */, 73); - node.node_ops = { - lookup: function(parent, name) { - var fd = +name; - var stream = FS.getStream(fd); - if (!stream) throw new FS.ErrnoError(8); - var ret = { - parent: null, - mount: { mountpoint: 'fake' }, - node_ops: { readlink: function() { return stream.path } } - }; - ret.parent = ret; // make it look like a simple root node - return ret; - } - }; - return node; - } - }, {}, '/proc/self/fd'); - },createStandardStreams:function() { - // TODO deprecate the old functionality of a single - // input / output callback and that utilizes FS.createDevice - // and instead require a unique set of stream ops - - // by default, we symlink the standard streams to the - // default tty devices. however, if the standard streams - // have been overwritten we create a unique device for - // them instead. - if (Module['stdin']) { - FS.createDevice('/dev', 'stdin', Module['stdin']); - } else { - FS.symlink('/dev/tty', '/dev/stdin'); - } - if (Module['stdout']) { - FS.createDevice('/dev', 'stdout', null, Module['stdout']); - } else { - FS.symlink('/dev/tty', '/dev/stdout'); - } - if (Module['stderr']) { - FS.createDevice('/dev', 'stderr', null, Module['stderr']); - } else { - FS.symlink('/dev/tty1', '/dev/stderr'); - } - - // open default streams for the stdin, stdout and stderr devices - var stdin = FS.open('/dev/stdin', 'r'); - var stdout = FS.open('/dev/stdout', 'w'); - var stderr = FS.open('/dev/stderr', 'w'); - },ensureErrnoError:function() { - if (FS.ErrnoError) return; - FS.ErrnoError = /** @this{Object} */ function ErrnoError(errno, node) { - this.node = node; - this.setErrno = /** @this{Object} */ function(errno) { - this.errno = errno; - }; - this.setErrno(errno); - this.message = 'FS error'; - - }; - FS.ErrnoError.prototype = new Error(); - FS.ErrnoError.prototype.constructor = FS.ErrnoError; - // Some errors may happen quite a bit, to avoid overhead we reuse them (and suffer a lack of stack info) - [44].forEach(function(code) { - FS.genericErrors[code] = new FS.ErrnoError(code); - FS.genericErrors[code].stack = ''; - }); - },staticInit:function() { - FS.ensureErrnoError(); - - FS.nameTable = new Array(4096); - - FS.mount(MEMFS, {}, '/'); - - FS.createDefaultDirectories(); - FS.createDefaultDevices(); - FS.createSpecialDirectories(); - - FS.filesystems = { - 'MEMFS': MEMFS, - }; - },init:function(input, output, error) { - FS.init.initialized = true; - - FS.ensureErrnoError(); - - // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here - Module['stdin'] = input || Module['stdin']; - Module['stdout'] = output || Module['stdout']; - Module['stderr'] = error || Module['stderr']; - - FS.createStandardStreams(); - },quit:function() { - FS.init.initialized = false; - // force-flush all streams, so we get musl std streams printed out - var fflush = Module['_fflush']; - if (fflush) fflush(0); - // close all of our streams - for (var i = 0; i < FS.streams.length; i++) { - var stream = FS.streams[i]; - if (!stream) { - continue; - } - FS.close(stream); - } - },getMode:function(canRead, canWrite) { - var mode = 0; - if (canRead) mode |= 292 | 73; - if (canWrite) mode |= 146; - return mode; - },joinPath:function(parts, forceRelative) { - var path = PATH.join.apply(null, parts); - if (forceRelative && path[0] == '/') path = path.substr(1); - return path; - },absolutePath:function(relative, base) { - return PATH_FS.resolve(base, relative); - },standardizePath:function(path) { - return PATH.normalize(path); - },findObject:function(path, dontResolveLastLink) { - var ret = FS.analyzePath(path, dontResolveLastLink); - if (ret.exists) { - return ret.object; - } else { - setErrNo(ret.error); - return null; - } - },analyzePath:function(path, dontResolveLastLink) { - // operate from within the context of the symlink's target - try { - var lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); - path = lookup.path; - } catch (e) { - } - var ret = { - isRoot: false, exists: false, error: 0, name: null, path: null, object: null, - parentExists: false, parentPath: null, parentObject: null - }; - try { - var lookup = FS.lookupPath(path, { parent: true }); - ret.parentExists = true; - ret.parentPath = lookup.path; - ret.parentObject = lookup.node; - ret.name = PATH.basename(path); - lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); - ret.exists = true; - ret.path = lookup.path; - ret.object = lookup.node; - ret.name = lookup.node.name; - ret.isRoot = lookup.path === '/'; - } catch (e) { - ret.error = e.errno; - }; - return ret; - },createFolder:function(parent, name, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(canRead, canWrite); - return FS.mkdir(path, mode); - },createPath:function(parent, path, canRead, canWrite) { - parent = typeof parent === 'string' ? parent : FS.getPath(parent); - var parts = path.split('/').reverse(); - while (parts.length) { - var part = parts.pop(); - if (!part) continue; - var current = PATH.join2(parent, part); - try { - FS.mkdir(current); - } catch (e) { - // ignore EEXIST - } - parent = current; - } - return current; - },createFile:function(parent, name, properties, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(canRead, canWrite); - return FS.create(path, mode); - },createDataFile:function(parent, name, data, canRead, canWrite, canOwn) { - var path = name ? PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name) : parent; - var mode = FS.getMode(canRead, canWrite); - var node = FS.create(path, mode); - if (data) { - if (typeof data === 'string') { - var arr = new Array(data.length); - for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); - data = arr; - } - // make sure we can write to the file - FS.chmod(node, mode | 146); - var stream = FS.open(node, 'w'); - FS.write(stream, data, 0, data.length, 0, canOwn); - FS.close(stream); - FS.chmod(node, mode); - } - return node; - },createDevice:function(parent, name, input, output) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(!!input, !!output); - if (!FS.createDevice.major) FS.createDevice.major = 64; - var dev = FS.makedev(FS.createDevice.major++, 0); - // Create a fake device that a set of stream ops to emulate - // the old behavior. - FS.registerDevice(dev, { - open: function(stream) { - stream.seekable = false; - }, - close: function(stream) { - // flush any pending line data - if (output && output.buffer && output.buffer.length) { - output(10); - } - }, - read: function(stream, buffer, offset, length, pos /* ignored */) { - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = input(); - } catch (e) { - throw new FS.ErrnoError(29); - } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError(6); - } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset+i] = result; - } - if (bytesRead) { - stream.node.timestamp = Date.now(); - } - return bytesRead; - }, - write: function(stream, buffer, offset, length, pos) { - for (var i = 0; i < length; i++) { - try { - output(buffer[offset+i]); - } catch (e) { - throw new FS.ErrnoError(29); - } - } - if (length) { - stream.node.timestamp = Date.now(); - } - return i; - } - }); - return FS.mkdev(path, mode, dev); - },createLink:function(parent, name, target, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - return FS.symlink(target, path); - },forceLoadFile:function(obj) { - if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; - var success = true; - if (typeof XMLHttpRequest !== 'undefined') { - throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); - } else if (read_) { - // Command-line. - try { - // WARNING: Can't read binary files in V8's d8 or tracemonkey's js, as - // read() will try to parse UTF8. - obj.contents = intArrayFromString(read_(obj.url), true); - obj.usedBytes = obj.contents.length; - } catch (e) { - success = false; - } - } else { - throw new Error('Cannot load without read() or XMLHttpRequest.'); - } - if (!success) setErrNo(29); - return success; - },createLazyFile:function(parent, name, url, canRead, canWrite) { - // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse. - /** @constructor */ - function LazyUint8Array() { - this.lengthKnown = false; - this.chunks = []; // Loaded chunks. Index is the chunk number - } - LazyUint8Array.prototype.get = /** @this{Object} */ function LazyUint8Array_get(idx) { - if (idx > this.length-1 || idx < 0) { - return undefined; - } - var chunkOffset = idx % this.chunkSize; - var chunkNum = (idx / this.chunkSize)|0; - return this.getter(chunkNum)[chunkOffset]; - }; - LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { - this.getter = getter; - }; - LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { - // Find length - var xhr = new XMLHttpRequest(); - xhr.open('HEAD', url, false); - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - var datalength = Number(xhr.getResponseHeader("Content-length")); - var header; - var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; - var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; - - var chunkSize = 1024*1024; // Chunk size in bytes - - if (!hasByteServing) chunkSize = datalength; - - // Function to get a range from the remote URL. - var doXHR = (function(from, to) { - if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); - - // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); - - // Some hints to the browser that we want binary data. - if (typeof Uint8Array != 'undefined') xhr.responseType = 'arraybuffer'; - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - } - - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - if (xhr.response !== undefined) { - return new Uint8Array(/** @type{Array} */(xhr.response || [])); - } else { - return intArrayFromString(xhr.responseText || '', true); - } - }); - var lazyArray = this; - lazyArray.setDataGetter(function(chunkNum) { - var start = chunkNum * chunkSize; - var end = (chunkNum+1) * chunkSize - 1; // including this byte - end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block - if (typeof(lazyArray.chunks[chunkNum]) === "undefined") { - lazyArray.chunks[chunkNum] = doXHR(start, end); - } - if (typeof(lazyArray.chunks[chunkNum]) === "undefined") throw new Error("doXHR failed!"); - return lazyArray.chunks[chunkNum]; - }); - - if (usesGzip || !datalength) { - // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length - chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file - datalength = this.getter(0).length; - chunkSize = datalength; - out("LazyFiles on gzip forces download of the whole file when length is accessed"); - } - - this._length = datalength; - this._chunkSize = chunkSize; - this.lengthKnown = true; - }; - if (typeof XMLHttpRequest !== 'undefined') { - if (!ENVIRONMENT_IS_WORKER) throw 'Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'; - var lazyArray = new LazyUint8Array(); - Object.defineProperties(lazyArray, { - length: { - get: /** @this{Object} */ function() { - if(!this.lengthKnown) { - this.cacheLength(); - } - return this._length; - } - }, - chunkSize: { - get: /** @this{Object} */ function() { - if(!this.lengthKnown) { - this.cacheLength(); - } - return this._chunkSize; - } - } - }); - - var properties = { isDevice: false, contents: lazyArray }; - } else { - var properties = { isDevice: false, url: url }; - } - - var node = FS.createFile(parent, name, properties, canRead, canWrite); - // This is a total hack, but I want to get this lazy file code out of the - // core of MEMFS. If we want to keep this lazy file concept I feel it should - // be its own thin LAZYFS proxying calls to MEMFS. - if (properties.contents) { - node.contents = properties.contents; - } else if (properties.url) { - node.contents = null; - node.url = properties.url; - } - // Add a function that defers querying the file size until it is asked the first time. - Object.defineProperties(node, { - usedBytes: { - get: /** @this {FSNode} */ function() { return this.contents.length; } - } - }); - // override each stream op with one that tries to force load the lazy file first - var stream_ops = {}; - var keys = Object.keys(node.stream_ops); - keys.forEach(function(key) { - var fn = node.stream_ops[key]; - stream_ops[key] = function forceLoadLazyFile() { - if (!FS.forceLoadFile(node)) { - throw new FS.ErrnoError(29); - } - return fn.apply(null, arguments); - }; - }); - // use a custom read function - stream_ops.read = function stream_ops_read(stream, buffer, offset, length, position) { - if (!FS.forceLoadFile(node)) { - throw new FS.ErrnoError(29); - } - var contents = stream.node.contents; - if (position >= contents.length) - return 0; - var size = Math.min(contents.length - position, length); - if (contents.slice) { // normal array - for (var i = 0; i < size; i++) { - buffer[offset + i] = contents[position + i]; - } - } else { - for (var i = 0; i < size; i++) { // LazyUint8Array from sync binary XHR - buffer[offset + i] = contents.get(position + i); - } - } - return size; - }; - node.stream_ops = stream_ops; - return node; - },createPreloadedFile:function(parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) { - Browser.init(); // XXX perhaps this method should move onto Browser? - // TODO we should allow people to just pass in a complete filename instead - // of parent and name being that we just join them anyways - var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; - var dep = getUniqueRunDependency('cp ' + fullname); // might have several active requests for the same fullname - function processData(byteArray) { - function finish(byteArray) { - if (preFinish) preFinish(); - if (!dontCreateFile) { - FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); - } - if (onload) onload(); - removeRunDependency(dep); - } - var handled = false; - Module['preloadPlugins'].forEach(function(plugin) { - if (handled) return; - if (plugin['canHandle'](fullname)) { - plugin['handle'](byteArray, fullname, finish, function() { - if (onerror) onerror(); - removeRunDependency(dep); - }); - handled = true; - } - }); - if (!handled) finish(byteArray); - } - addRunDependency(dep); - if (typeof url == 'string') { - Browser.asyncLoad(url, function(byteArray) { - processData(byteArray); - }, onerror); - } else { - processData(url); - } - },indexedDB:function() { - return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - },DB_NAME:function() { - return 'EM_FS_' + window.location.pathname; - },DB_VERSION:20,DB_STORE_NAME:"FILE_DATA",saveFilesToDB:function(paths, onload, onerror) { - onload = onload || function(){}; - onerror = onerror || function(){}; - var indexedDB = FS.indexedDB(); - try { - var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); - } catch (e) { - return onerror(e); - } - openRequest.onupgradeneeded = function openRequest_onupgradeneeded() { - out('creating db'); - var db = openRequest.result; - db.createObjectStore(FS.DB_STORE_NAME); - }; - openRequest.onsuccess = function openRequest_onsuccess() { - var db = openRequest.result; - var transaction = db.transaction([FS.DB_STORE_NAME], 'readwrite'); - var files = transaction.objectStore(FS.DB_STORE_NAME); - var ok = 0, fail = 0, total = paths.length; - function finish() { - if (fail == 0) onload(); else onerror(); - } - paths.forEach(function(path) { - var putRequest = files.put(FS.analyzePath(path).object.contents, path); - putRequest.onsuccess = function putRequest_onsuccess() { ok++; if (ok + fail == total) finish() }; - putRequest.onerror = function putRequest_onerror() { fail++; if (ok + fail == total) finish() }; - }); - transaction.onerror = onerror; - }; - openRequest.onerror = onerror; - },loadFilesFromDB:function(paths, onload, onerror) { - onload = onload || function(){}; - onerror = onerror || function(){}; - var indexedDB = FS.indexedDB(); - try { - var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); - } catch (e) { - return onerror(e); - } - openRequest.onupgradeneeded = onerror; // no database to load from - openRequest.onsuccess = function openRequest_onsuccess() { - var db = openRequest.result; - try { - var transaction = db.transaction([FS.DB_STORE_NAME], 'readonly'); - } catch(e) { - onerror(e); - return; - } - var files = transaction.objectStore(FS.DB_STORE_NAME); - var ok = 0, fail = 0, total = paths.length; - function finish() { - if (fail == 0) onload(); else onerror(); - } - paths.forEach(function(path) { - var getRequest = files.get(path); - getRequest.onsuccess = function getRequest_onsuccess() { - if (FS.analyzePath(path).exists) { - FS.unlink(path); - } - FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true); - ok++; - if (ok + fail == total) finish(); - }; - getRequest.onerror = function getRequest_onerror() { fail++; if (ok + fail == total) finish() }; - }); - transaction.onerror = onerror; - }; - openRequest.onerror = onerror; - }};var SYSCALLS={mappings:{},DEFAULT_POLLMASK:5,umask:511,calculateAt:function(dirfd, path) { - if (path[0] !== '/') { - // relative path - var dir; - if (dirfd === -100) { - dir = FS.cwd(); - } else { - var dirstream = FS.getStream(dirfd); - if (!dirstream) throw new FS.ErrnoError(8); - dir = dirstream.path; - } - path = PATH.join2(dir, path); - } - return path; - },doStat:function(func, path, buf) { - try { - var stat = func(path); - } catch (e) { - if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { - // an error occurred while trying to look up the path; we should just report ENOTDIR - return -54; - } - throw e; - } - HEAP32[((buf)>>2)]=stat.dev; - HEAP32[(((buf)+(4))>>2)]=0; - HEAP32[(((buf)+(8))>>2)]=stat.ino; - HEAP32[(((buf)+(12))>>2)]=stat.mode; - HEAP32[(((buf)+(16))>>2)]=stat.nlink; - HEAP32[(((buf)+(20))>>2)]=stat.uid; - HEAP32[(((buf)+(24))>>2)]=stat.gid; - HEAP32[(((buf)+(28))>>2)]=stat.rdev; - HEAP32[(((buf)+(32))>>2)]=0; - (tempI64 = [stat.size>>>0,(tempDouble=stat.size,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[(((buf)+(40))>>2)]=tempI64[0],HEAP32[(((buf)+(44))>>2)]=tempI64[1]); - HEAP32[(((buf)+(48))>>2)]=4096; - HEAP32[(((buf)+(52))>>2)]=stat.blocks; - HEAP32[(((buf)+(56))>>2)]=(stat.atime.getTime() / 1000)|0; - HEAP32[(((buf)+(60))>>2)]=0; - HEAP32[(((buf)+(64))>>2)]=(stat.mtime.getTime() / 1000)|0; - HEAP32[(((buf)+(68))>>2)]=0; - HEAP32[(((buf)+(72))>>2)]=(stat.ctime.getTime() / 1000)|0; - HEAP32[(((buf)+(76))>>2)]=0; - (tempI64 = [stat.ino>>>0,(tempDouble=stat.ino,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[(((buf)+(80))>>2)]=tempI64[0],HEAP32[(((buf)+(84))>>2)]=tempI64[1]); - return 0; - },doMsync:function(addr, stream, len, flags, offset) { - var buffer = HEAPU8.slice(addr, addr + len); - FS.msync(stream, buffer, offset, len, flags); - },doMkdir:function(path, mode) { - // remove a trailing slash, if one - /a/b/ has basename of '', but - // we want to create b in the context of this function - path = PATH.normalize(path); - if (path[path.length-1] === '/') path = path.substr(0, path.length-1); - FS.mkdir(path, mode, 0); - return 0; - },doMknod:function(path, mode, dev) { - // we don't want this in the JS API as it uses mknod to create all nodes. - switch (mode & 61440) { - case 32768: - case 8192: - case 24576: - case 4096: - case 49152: - break; - default: return -28; - } - FS.mknod(path, mode, dev); - return 0; - },doReadlink:function(path, buf, bufsize) { - if (bufsize <= 0) return -28; - var ret = FS.readlink(path); - - var len = Math.min(bufsize, lengthBytesUTF8(ret)); - var endChar = HEAP8[buf+len]; - stringToUTF8(ret, buf, bufsize+1); - // readlink is one of the rare functions that write out a C string, but does never append a null to the output buffer(!) - // stringToUTF8() always appends a null byte, so restore the character under the null byte after the write. - HEAP8[buf+len] = endChar; - - return len; - },doAccess:function(path, amode) { - if (amode & ~7) { - // need a valid mode - return -28; - } - var node; - var lookup = FS.lookupPath(path, { follow: true }); - node = lookup.node; - if (!node) { - return -44; - } - var perms = ''; - if (amode & 4) perms += 'r'; - if (amode & 2) perms += 'w'; - if (amode & 1) perms += 'x'; - if (perms /* otherwise, they've just passed F_OK */ && FS.nodePermissions(node, perms)) { - return -2; - } - return 0; - },doDup:function(path, flags, suggestFD) { - var suggest = FS.getStream(suggestFD); - if (suggest) FS.close(suggest); - return FS.open(path, flags, 0, suggestFD, suggestFD).fd; - },doReadv:function(stream, iov, iovcnt, offset) { - var ret = 0; - for (var i = 0; i < iovcnt; i++) { - var ptr = HEAP32[(((iov)+(i*8))>>2)]; - var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; - var curr = FS.read(stream, HEAP8,ptr, len, offset); - if (curr < 0) return -1; - ret += curr; - if (curr < len) break; // nothing more to read - } - return ret; - },doWritev:function(stream, iov, iovcnt, offset) { - var ret = 0; - for (var i = 0; i < iovcnt; i++) { - var ptr = HEAP32[(((iov)+(i*8))>>2)]; - var len = HEAP32[(((iov)+(i*8 + 4))>>2)]; - var curr = FS.write(stream, HEAP8,ptr, len, offset); - if (curr < 0) return -1; - ret += curr; - } - return ret; - },varargs:undefined,get:function() { - SYSCALLS.varargs += 4; - var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; - return ret; - },getStr:function(ptr) { - var ret = UTF8ToString(ptr); - return ret; - },getStreamFromFD:function(fd) { - var stream = FS.getStream(fd); - if (!stream) throw new FS.ErrnoError(8); - return stream; - },get64:function(low, high) { - return low; - }};function _fd_close(fd) {try { - - var stream = SYSCALLS.getStreamFromFD(fd); - FS.close(stream); - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return e.errno; - } - } - - function _fd_read(fd, iov, iovcnt, pnum) {try { - - var stream = SYSCALLS.getStreamFromFD(fd); - var num = SYSCALLS.doReadv(stream, iov, iovcnt); - HEAP32[((pnum)>>2)]=num - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return e.errno; - } - } - - function _fd_seek(fd, offset_low, offset_high, whence, newOffset) {try { - - - var stream = SYSCALLS.getStreamFromFD(fd); - var HIGH_OFFSET = 0x100000000; // 2^32 - // use an unsigned operator on low and shift high by 32-bits - var offset = offset_high * HIGH_OFFSET + (offset_low >>> 0); - - var DOUBLE_LIMIT = 0x20000000000000; // 2^53 - // we also check for equality since DOUBLE_LIMIT + 1 == DOUBLE_LIMIT - if (offset <= -DOUBLE_LIMIT || offset >= DOUBLE_LIMIT) { - return -61; - } - - FS.llseek(stream, offset, whence); - (tempI64 = [stream.position>>>0,(tempDouble=stream.position,(+(Math_abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math_min((+(Math_floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((newOffset)>>2)]=tempI64[0],HEAP32[(((newOffset)+(4))>>2)]=tempI64[1]); - if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; // reset readdir state - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return e.errno; - } - } - - function _fd_write(fd, iov, iovcnt, pnum) {try { - - var stream = SYSCALLS.getStreamFromFD(fd); - var num = SYSCALLS.doWritev(stream, iov, iovcnt); - HEAP32[((pnum)>>2)]=num - return 0; - } catch (e) { - if (typeof FS === 'undefined' || !(e instanceof FS.ErrnoError)) abort(e); - return e.errno; - } - } - - - function _round(d) { - d = +d; - return d >= +0 ? +Math_floor(d + +0.5) : +Math_ceil(d - +0.5); - } - - function _setTempRet0($i) { - setTempRet0(($i) | 0); - } -var FSNode = /** @constructor */ function(parent, name, mode, rdev) { - if (!parent) { - parent = this; // root node sets parent to itself - } - this.parent = parent; - this.mount = parent.mount; - this.mounted = null; - this.id = FS.nextInode++; - this.name = name; - this.mode = mode; - this.node_ops = {}; - this.stream_ops = {}; - this.rdev = rdev; - }; - var readMode = 292/*292*/ | 73/*73*/; - var writeMode = 146/*146*/; - Object.defineProperties(FSNode.prototype, { - read: { - get: /** @this{FSNode} */function() { - return (this.mode & readMode) === readMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= readMode : this.mode &= ~readMode; - } - }, - write: { - get: /** @this{FSNode} */function() { - return (this.mode & writeMode) === writeMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= writeMode : this.mode &= ~writeMode; - } - }, - isFolder: { - get: /** @this{FSNode} */function() { - return FS.isDir(this.mode); - } - }, - isDevice: { - get: /** @this{FSNode} */function() { - return FS.isChrdev(this.mode); - } - } - }); - FS.FSNode = FSNode; - FS.staticInit();; -var ASSERTIONS = false; - - - -/** @type {function(string, boolean=, number=)} */ -function intArrayFromString(stringy, dontAddNull, length) { - var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; - var u8array = new Array(len); - var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); - if (dontAddNull) u8array.length = numBytesWritten; - return u8array; -} - -function intArrayToString(array) { - var ret = []; - for (var i = 0; i < array.length; i++) { - var chr = array[i]; - if (chr > 0xFF) { - if (ASSERTIONS) { - assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.'); - } - chr &= 0xFF; - } - ret.push(String.fromCharCode(chr)); - } - return ret.join(''); -} - - -// Copied from https://github.com/strophe/strophejs/blob/e06d027/src/polyfills.js#L149 - -// This code was written by Tyler Akins and has been placed in the -// public domain. It would be nice if you left this header intact. -// Base64 code from Tyler Akins -- http://rumkin.com - -/** - * Decodes a base64 string. - * @param {string} input The string to decode. - */ -var decodeBase64 = typeof atob === 'function' ? atob : function (input) { - var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - - var output = ''; - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0; - // remove all characters that are not A-Z, a-z, 0-9, +, /, or = - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); - do { - enc1 = keyStr.indexOf(input.charAt(i++)); - enc2 = keyStr.indexOf(input.charAt(i++)); - enc3 = keyStr.indexOf(input.charAt(i++)); - enc4 = keyStr.indexOf(input.charAt(i++)); - - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - - output = output + String.fromCharCode(chr1); - - if (enc3 !== 64) { - output = output + String.fromCharCode(chr2); - } - if (enc4 !== 64) { - output = output + String.fromCharCode(chr3); - } - } while (i < input.length); - return output; -}; - -// Converts a string of base64 into a byte array. -// Throws error on invalid input. -function intArrayFromBase64(s) { - if (typeof ENVIRONMENT_IS_NODE === 'boolean' && ENVIRONMENT_IS_NODE) { - var buf; - try { - // TODO: Update Node.js externs, Closure does not recognize the following Buffer.from() - /**@suppress{checkTypes}*/ - buf = Buffer.from(s, 'base64'); - } catch (_) { - buf = new Buffer(s, 'base64'); - } - return new Uint8Array(buf['buffer'], buf['byteOffset'], buf['byteLength']); - } - - try { - var decoded = decodeBase64(s); - var bytes = new Uint8Array(decoded.length); - for (var i = 0 ; i < decoded.length ; ++i) { - bytes[i] = decoded.charCodeAt(i); - } - return bytes; - } catch (_) { - throw new Error('Converting base64 string to bytes failed.'); - } -} - -// If filename is a base64 data URI, parses and returns data (Buffer on node, -// Uint8Array otherwise). If filename is not a base64 data URI, returns undefined. -function tryParseAsDataURI(filename) { - if (!isDataURI(filename)) { - return; - } - - return intArrayFromBase64(filename.slice(dataURIPrefix.length)); -} - - -// ASM_LIBRARY EXTERN PRIMITIVES: Math_floor,Math_ceil - -var asmGlobalArg = {}; -var asmLibraryArg = { "emscripten_get_sbrk_ptr": _emscripten_get_sbrk_ptr, "emscripten_memcpy_big": _emscripten_memcpy_big, "emscripten_resize_heap": _emscripten_resize_heap, "fd_close": _fd_close, "fd_read": _fd_read, "fd_seek": _fd_seek, "fd_write": _fd_write, "getTempRet0": getTempRet0, "memory": wasmMemory, "round": _round, "setTempRet0": setTempRet0, "table": wasmTable }; -var asm = createWasm(); -/** @type {function(...*):?} */ -var ___wasm_call_ctors = Module["___wasm_call_ctors"] = function() { - return (___wasm_call_ctors = Module["___wasm_call_ctors"] = Module["asm"]["__wasm_call_ctors"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_new = Module["_FLAC__stream_decoder_new"] = function() { - return (_FLAC__stream_decoder_new = Module["_FLAC__stream_decoder_new"] = Module["asm"]["FLAC__stream_decoder_new"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_delete = Module["_FLAC__stream_decoder_delete"] = function() { - return (_FLAC__stream_decoder_delete = Module["_FLAC__stream_decoder_delete"] = Module["asm"]["FLAC__stream_decoder_delete"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_finish = Module["_FLAC__stream_decoder_finish"] = function() { - return (_FLAC__stream_decoder_finish = Module["_FLAC__stream_decoder_finish"] = Module["asm"]["FLAC__stream_decoder_finish"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_init_stream = Module["_FLAC__stream_decoder_init_stream"] = function() { - return (_FLAC__stream_decoder_init_stream = Module["_FLAC__stream_decoder_init_stream"] = Module["asm"]["FLAC__stream_decoder_init_stream"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_reset = Module["_FLAC__stream_decoder_reset"] = function() { - return (_FLAC__stream_decoder_reset = Module["_FLAC__stream_decoder_reset"] = Module["asm"]["FLAC__stream_decoder_reset"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_init_ogg_stream = Module["_FLAC__stream_decoder_init_ogg_stream"] = function() { - return (_FLAC__stream_decoder_init_ogg_stream = Module["_FLAC__stream_decoder_init_ogg_stream"] = Module["asm"]["FLAC__stream_decoder_init_ogg_stream"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_ogg_serial_number = Module["_FLAC__stream_decoder_set_ogg_serial_number"] = function() { - return (_FLAC__stream_decoder_set_ogg_serial_number = Module["_FLAC__stream_decoder_set_ogg_serial_number"] = Module["asm"]["FLAC__stream_decoder_set_ogg_serial_number"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_md5_checking = Module["_FLAC__stream_decoder_set_md5_checking"] = function() { - return (_FLAC__stream_decoder_set_md5_checking = Module["_FLAC__stream_decoder_set_md5_checking"] = Module["asm"]["FLAC__stream_decoder_set_md5_checking"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_respond = Module["_FLAC__stream_decoder_set_metadata_respond"] = function() { - return (_FLAC__stream_decoder_set_metadata_respond = Module["_FLAC__stream_decoder_set_metadata_respond"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_respond_application = Module["_FLAC__stream_decoder_set_metadata_respond_application"] = function() { - return (_FLAC__stream_decoder_set_metadata_respond_application = Module["_FLAC__stream_decoder_set_metadata_respond_application"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond_application"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_respond_all = Module["_FLAC__stream_decoder_set_metadata_respond_all"] = function() { - return (_FLAC__stream_decoder_set_metadata_respond_all = Module["_FLAC__stream_decoder_set_metadata_respond_all"] = Module["asm"]["FLAC__stream_decoder_set_metadata_respond_all"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_ignore = Module["_FLAC__stream_decoder_set_metadata_ignore"] = function() { - return (_FLAC__stream_decoder_set_metadata_ignore = Module["_FLAC__stream_decoder_set_metadata_ignore"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_ignore_application = Module["_FLAC__stream_decoder_set_metadata_ignore_application"] = function() { - return (_FLAC__stream_decoder_set_metadata_ignore_application = Module["_FLAC__stream_decoder_set_metadata_ignore_application"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore_application"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_set_metadata_ignore_all = Module["_FLAC__stream_decoder_set_metadata_ignore_all"] = function() { - return (_FLAC__stream_decoder_set_metadata_ignore_all = Module["_FLAC__stream_decoder_set_metadata_ignore_all"] = Module["asm"]["FLAC__stream_decoder_set_metadata_ignore_all"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_get_state = Module["_FLAC__stream_decoder_get_state"] = function() { - return (_FLAC__stream_decoder_get_state = Module["_FLAC__stream_decoder_get_state"] = Module["asm"]["FLAC__stream_decoder_get_state"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_get_md5_checking = Module["_FLAC__stream_decoder_get_md5_checking"] = function() { - return (_FLAC__stream_decoder_get_md5_checking = Module["_FLAC__stream_decoder_get_md5_checking"] = Module["asm"]["FLAC__stream_decoder_get_md5_checking"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_process_single = Module["_FLAC__stream_decoder_process_single"] = function() { - return (_FLAC__stream_decoder_process_single = Module["_FLAC__stream_decoder_process_single"] = Module["asm"]["FLAC__stream_decoder_process_single"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_process_until_end_of_metadata = Module["_FLAC__stream_decoder_process_until_end_of_metadata"] = function() { - return (_FLAC__stream_decoder_process_until_end_of_metadata = Module["_FLAC__stream_decoder_process_until_end_of_metadata"] = Module["asm"]["FLAC__stream_decoder_process_until_end_of_metadata"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_decoder_process_until_end_of_stream = Module["_FLAC__stream_decoder_process_until_end_of_stream"] = function() { - return (_FLAC__stream_decoder_process_until_end_of_stream = Module["_FLAC__stream_decoder_process_until_end_of_stream"] = Module["asm"]["FLAC__stream_decoder_process_until_end_of_stream"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_new = Module["_FLAC__stream_encoder_new"] = function() { - return (_FLAC__stream_encoder_new = Module["_FLAC__stream_encoder_new"] = Module["asm"]["FLAC__stream_encoder_new"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_delete = Module["_FLAC__stream_encoder_delete"] = function() { - return (_FLAC__stream_encoder_delete = Module["_FLAC__stream_encoder_delete"] = Module["asm"]["FLAC__stream_encoder_delete"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_finish = Module["_FLAC__stream_encoder_finish"] = function() { - return (_FLAC__stream_encoder_finish = Module["_FLAC__stream_encoder_finish"] = Module["asm"]["FLAC__stream_encoder_finish"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_init_stream = Module["_FLAC__stream_encoder_init_stream"] = function() { - return (_FLAC__stream_encoder_init_stream = Module["_FLAC__stream_encoder_init_stream"] = Module["asm"]["FLAC__stream_encoder_init_stream"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_init_ogg_stream = Module["_FLAC__stream_encoder_init_ogg_stream"] = function() { - return (_FLAC__stream_encoder_init_ogg_stream = Module["_FLAC__stream_encoder_init_ogg_stream"] = Module["asm"]["FLAC__stream_encoder_init_ogg_stream"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_ogg_serial_number = Module["_FLAC__stream_encoder_set_ogg_serial_number"] = function() { - return (_FLAC__stream_encoder_set_ogg_serial_number = Module["_FLAC__stream_encoder_set_ogg_serial_number"] = Module["asm"]["FLAC__stream_encoder_set_ogg_serial_number"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_verify = Module["_FLAC__stream_encoder_set_verify"] = function() { - return (_FLAC__stream_encoder_set_verify = Module["_FLAC__stream_encoder_set_verify"] = Module["asm"]["FLAC__stream_encoder_set_verify"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_channels = Module["_FLAC__stream_encoder_set_channels"] = function() { - return (_FLAC__stream_encoder_set_channels = Module["_FLAC__stream_encoder_set_channels"] = Module["asm"]["FLAC__stream_encoder_set_channels"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_bits_per_sample = Module["_FLAC__stream_encoder_set_bits_per_sample"] = function() { - return (_FLAC__stream_encoder_set_bits_per_sample = Module["_FLAC__stream_encoder_set_bits_per_sample"] = Module["asm"]["FLAC__stream_encoder_set_bits_per_sample"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_sample_rate = Module["_FLAC__stream_encoder_set_sample_rate"] = function() { - return (_FLAC__stream_encoder_set_sample_rate = Module["_FLAC__stream_encoder_set_sample_rate"] = Module["asm"]["FLAC__stream_encoder_set_sample_rate"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_compression_level = Module["_FLAC__stream_encoder_set_compression_level"] = function() { - return (_FLAC__stream_encoder_set_compression_level = Module["_FLAC__stream_encoder_set_compression_level"] = Module["asm"]["FLAC__stream_encoder_set_compression_level"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_blocksize = Module["_FLAC__stream_encoder_set_blocksize"] = function() { - return (_FLAC__stream_encoder_set_blocksize = Module["_FLAC__stream_encoder_set_blocksize"] = Module["asm"]["FLAC__stream_encoder_set_blocksize"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_total_samples_estimate = Module["_FLAC__stream_encoder_set_total_samples_estimate"] = function() { - return (_FLAC__stream_encoder_set_total_samples_estimate = Module["_FLAC__stream_encoder_set_total_samples_estimate"] = Module["asm"]["FLAC__stream_encoder_set_total_samples_estimate"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_set_metadata = Module["_FLAC__stream_encoder_set_metadata"] = function() { - return (_FLAC__stream_encoder_set_metadata = Module["_FLAC__stream_encoder_set_metadata"] = Module["asm"]["FLAC__stream_encoder_set_metadata"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_get_state = Module["_FLAC__stream_encoder_get_state"] = function() { - return (_FLAC__stream_encoder_get_state = Module["_FLAC__stream_encoder_get_state"] = Module["asm"]["FLAC__stream_encoder_get_state"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_get_verify_decoder_state = Module["_FLAC__stream_encoder_get_verify_decoder_state"] = function() { - return (_FLAC__stream_encoder_get_verify_decoder_state = Module["_FLAC__stream_encoder_get_verify_decoder_state"] = Module["asm"]["FLAC__stream_encoder_get_verify_decoder_state"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_get_verify = Module["_FLAC__stream_encoder_get_verify"] = function() { - return (_FLAC__stream_encoder_get_verify = Module["_FLAC__stream_encoder_get_verify"] = Module["asm"]["FLAC__stream_encoder_get_verify"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_process = Module["_FLAC__stream_encoder_process"] = function() { - return (_FLAC__stream_encoder_process = Module["_FLAC__stream_encoder_process"] = Module["asm"]["FLAC__stream_encoder_process"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _FLAC__stream_encoder_process_interleaved = Module["_FLAC__stream_encoder_process_interleaved"] = function() { - return (_FLAC__stream_encoder_process_interleaved = Module["_FLAC__stream_encoder_process_interleaved"] = Module["asm"]["FLAC__stream_encoder_process_interleaved"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var ___errno_location = Module["___errno_location"] = function() { - return (___errno_location = Module["___errno_location"] = Module["asm"]["__errno_location"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var stackSave = Module["stackSave"] = function() { - return (stackSave = Module["stackSave"] = Module["asm"]["stackSave"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var stackRestore = Module["stackRestore"] = function() { - return (stackRestore = Module["stackRestore"] = Module["asm"]["stackRestore"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var stackAlloc = Module["stackAlloc"] = function() { - return (stackAlloc = Module["stackAlloc"] = Module["asm"]["stackAlloc"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _malloc = Module["_malloc"] = function() { - return (_malloc = Module["_malloc"] = Module["asm"]["malloc"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _free = Module["_free"] = function() { - return (_free = Module["_free"] = Module["asm"]["free"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var __growWasmMemory = Module["__growWasmMemory"] = function() { - return (__growWasmMemory = Module["__growWasmMemory"] = Module["asm"]["__growWasmMemory"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_iii = Module["dynCall_iii"] = function() { - return (dynCall_iii = Module["dynCall_iii"] = Module["asm"]["dynCall_iii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_ii = Module["dynCall_ii"] = function() { - return (dynCall_ii = Module["dynCall_ii"] = Module["asm"]["dynCall_ii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_iiii = Module["dynCall_iiii"] = function() { - return (dynCall_iiii = Module["dynCall_iiii"] = Module["asm"]["dynCall_iiii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_jiji = Module["dynCall_jiji"] = function() { - return (dynCall_jiji = Module["dynCall_jiji"] = Module["asm"]["dynCall_jiji"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_viiiiii = Module["dynCall_viiiiii"] = function() { - return (dynCall_viiiiii = Module["dynCall_viiiiii"] = Module["asm"]["dynCall_viiiiii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_iiiii = Module["dynCall_iiiii"] = function() { - return (dynCall_iiiii = Module["dynCall_iiiii"] = Module["asm"]["dynCall_iiiii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_viiiiiii = Module["dynCall_viiiiiii"] = function() { - return (dynCall_viiiiiii = Module["dynCall_viiiiiii"] = Module["asm"]["dynCall_viiiiiii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_viiii = Module["dynCall_viiii"] = function() { - return (dynCall_viiii = Module["dynCall_viiii"] = Module["asm"]["dynCall_viiii"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var dynCall_viii = Module["dynCall_viii"] = function() { - return (dynCall_viii = Module["dynCall_viii"] = Module["asm"]["dynCall_viii"]).apply(null, arguments); -}; - - - - - -// === Auto-generated postamble setup entry stuff === - - - - -Module["ccall"] = ccall; -Module["cwrap"] = cwrap; -Module["setValue"] = setValue; -Module["getValue"] = getValue; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -var calledRun; - -/** - * @constructor - * @this {ExitStatus} - */ -function ExitStatus(status) { - this.name = "ExitStatus"; - this.message = "Program terminated with exit(" + status + ")"; - this.status = status; -} - -var calledMain = false; - - -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!calledRun) run(); - if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled -}; - - - - - -/** @type {function(Array=)} */ -function run(args) { - args = args || arguments_; - - if (runDependencies > 0) { - return; - } - - - preRun(); - - if (runDependencies > 0) return; // a preRun added a dependency, run will be called later - - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - if (calledRun) return; - calledRun = true; - Module['calledRun'] = true; - - if (ABORT) return; - - initRuntime(); - - preMain(); - - if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); - - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); - doRun(); - }, 1); - } else - { - doRun(); - } -} -Module['run'] = run; - - -/** @param {boolean|number=} implicit */ -function exit(status, implicit) { - - // if this is just main exit-ing implicitly, and the status is 0, then we - // don't need to do anything here and can just leave. if the status is - // non-zero, though, then we need to report it. - // (we may have warned about this earlier, if a situation justifies doing so) - if (implicit && noExitRuntime && status === 0) { - return; - } - - if (noExitRuntime) { - } else { - - ABORT = true; - EXITSTATUS = status; - - exitRuntime(); - - if (Module['onExit']) Module['onExit'](status); - } - - quit_(status, new ExitStatus(status)); -} - -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - - - noExitRuntime = true; - -run(); - - - - - - -// {{MODULE_ADDITIONS}} - - - -//libflac function wrappers - -/** - * HELPER read/extract stream info meta-data from frame header / meta-data - * @param {POINTER} p_streaminfo - * @returns StreamInfo - */ -function _readStreamInfo(p_streaminfo){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_STREAMINFO (0) - - /* - typedef struct { - unsigned min_blocksize, max_blocksize; - unsigned min_framesize, max_framesize; - unsigned sample_rate; - unsigned channels; - unsigned bits_per_sample; - FLAC__uint64 total_samples; - FLAC__byte md5sum[16]; - } FLAC__StreamMetadata_StreamInfo; - */ - - var min_blocksize = Module.getValue(p_streaminfo,'i32');//4 bytes - var max_blocksize = Module.getValue(p_streaminfo+4,'i32');//4 bytes - - var min_framesize = Module.getValue(p_streaminfo+8,'i32');//4 bytes - var max_framesize = Module.getValue(p_streaminfo+12,'i32');//4 bytes - - var sample_rate = Module.getValue(p_streaminfo+16,'i32');//4 bytes - var channels = Module.getValue(p_streaminfo+20,'i32');//4 bytes - - var bits_per_sample = Module.getValue(p_streaminfo+24,'i32');//4 bytes - - //FIXME should be at p_streaminfo+28, but seems to be at p_streaminfo+32 - var total_samples = Module.getValue(p_streaminfo+32,'i64');//8 bytes - - var md5sum = _readMd5(p_streaminfo+40);//16 bytes - - return { - min_blocksize: min_blocksize, - max_blocksize: max_blocksize, - min_framesize: min_framesize, - max_framesize: max_framesize, - sampleRate: sample_rate, - channels: channels, - bitsPerSample: bits_per_sample, - total_samples: total_samples, - md5sum: md5sum - }; -} - -/** - * read MD5 checksum - * @param {POINTER} p_md5 - * @returns {String} as HEX string representation - */ -function _readMd5(p_md5){ - - var sb = [], v, str; - for(var i=0, len = 16; i < len; ++i){ - v = Module.getValue(p_md5+i,'i8');//1 byte - if(v < 0) v = 256 + v;//<- "convert" to uint8, if necessary - str = v.toString(16); - if(str.length < 2) str = '0' + str;//<- add padding, if necessary - sb.push(str); - } - return sb.join(''); -} - -/** - * HELPER: read frame data - * - * @param {POINTER} p_frame - * @param {CodingOptions} [enc_opt] - * @returns FrameHeader - */ -function _readFrameHdr(p_frame, enc_opt){ - - /* - typedef struct { - unsigned blocksize; - unsigned sample_rate; - unsigned channels; - FLAC__ChannelAssignment channel_assignment; - unsigned bits_per_sample; - FLAC__FrameNumberType number_type; - union { - FLAC__uint32 frame_number; - FLAC__uint64 sample_number; - } number; - FLAC__uint8 crc; - } FLAC__FrameHeader; - */ - - var blocksize = Module.getValue(p_frame,'i32');//4 bytes - var sample_rate = Module.getValue(p_frame+4,'i32');//4 bytes - var channels = Module.getValue(p_frame+8,'i32');//4 bytes - - // 0: FLAC__CHANNEL_ASSIGNMENT_INDEPENDENT independent channels - // 1: FLAC__CHANNEL_ASSIGNMENT_LEFT_SIDE left+side stereo - // 2: FLAC__CHANNEL_ASSIGNMENT_RIGHT_SIDE right+side stereo - // 3: FLAC__CHANNEL_ASSIGNMENT_MID_SIDE mid+side stereo - var channel_assignment = Module.getValue(p_frame+12,'i32');//4 bytes - - var bits_per_sample = Module.getValue(p_frame+16,'i32'); - - // 0: FLAC__FRAME_NUMBER_TYPE_FRAME_NUMBER number contains the frame number - // 1: FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER number contains the sample number of first sample in frame - var number_type = Module.getValue(p_frame+20,'i32'); - - // union {} number: The frame number or sample number of first sample in frame; use the number_type value to determine which to use. - var frame_number = Module.getValue(p_frame+24,'i32'); - var sample_number = Module.getValue(p_frame+24,'i64'); - - var number = number_type === 0? frame_number : sample_number; - var numberType = number_type === 0? 'frames' : 'samples'; - - var crc = Module.getValue(p_frame+36,'i8'); - - var subframes; - if(enc_opt && enc_opt.analyseSubframes){ - var subOffset = {offset: 40}; - subframes = []; - for(var i=0; i < channels; ++i){ - subframes.push(_readSubFrameHdr(p_frame, subOffset, blocksize, enc_opt)); - } - //TODO read footer - // console.log(' footer crc ', Module.getValue(p_frame + subOffset.offset,'i16')); - } - - return { - blocksize: blocksize, - sampleRate: sample_rate, - channels: channels, - channelAssignment: channel_assignment, - bitsPerSample: bits_per_sample, - number: number, - numberType: numberType, - crc: crc, - subframes: subframes - }; -} - - -function _readSubFrameHdr(p_subframe, subOffset, block_size, enc_opt){ - /* - FLAC__SubframeType type - union { - FLAC__Subframe_Constant constant - FLAC__Subframe_Fixed fixed - FLAC__Subframe_LPC lpc - FLAC__Subframe_Verbatim verbatim - } data - unsigned wasted_bits - */ - - var type = Module.getValue(p_subframe + subOffset.offset, 'i32'); - subOffset.offset += 4; - - var data; - switch(type){ - case 0: //FLAC__SUBFRAME_TYPE_CONSTANT - data = {value: Module.getValue(p_subframe + subOffset.offset, 'i32')}; - subOffset.offset += 284;//4; - break; - case 1: //FLAC__SUBFRAME_TYPE_VERBATIM - data = Module.getValue(p_subframe + subOffset.offset, 'i32'); - subOffset.offset += 284;//4; - break; - case 2: //FLAC__SUBFRAME_TYPE_FIXED - data = _readSubFrameHdrFixedData(p_subframe, subOffset, block_size, false, enc_opt); - break; - case 3: //FLAC__SUBFRAME_TYPE_LPC - data = _readSubFrameHdrFixedData(p_subframe, subOffset, block_size, true, enc_opt); - break; - } - - var offset = subOffset.offset; - var wasted_bits = Module.getValue(p_subframe + offset, 'i32'); - subOffset.offset += 4; - - return { - type: type,//['CONSTANT', 'VERBATIM', 'FIXED', 'LPC'][type], - data: data, - wastedBits: wasted_bits - } -} - -function _readSubFrameHdrFixedData(p_subframe_data, subOffset, block_size, is_lpc, enc_opt){ - - var offset = subOffset.offset; - - var data = {order: -1, contents: {parameters: [], rawBits: []}}; - //FLAC__Subframe_Fixed: - // FLAC__EntropyCodingMethod entropy_coding_method - // unsigned order - // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER] - // const FLAC__int32 * residual - - //FLAC__EntropyCodingMethod: - // FLAC__EntropyCodingMethodType type - // union { - // FLAC__EntropyCodingMethod_PartitionedRice partitioned_rice - // } data - - //FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE 0 Residual is coded by partitioning into contexts, each with it's own 4-bit Rice parameter. - //FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2 1 Residual is coded by partitioning into contexts, each with it's own 5-bit Rice parameter. - var entropyType = Module.getValue(p_subframe_data, 'i32'); - offset += 4; - - //FLAC__EntropyCodingMethod_PartitionedRice: - // unsigned order - var entropyOrder = Module.getValue(p_subframe_data + offset, 'i32'); - data.order = entropyOrder; - offset += 4; - - //FLAC__EntropyCodingMethod_PartitionedRice: - // FLAC__EntropyCodingMethod_PartitionedRiceContents * contents - var partitions = 1 << entropyOrder, params = data.contents.parameters, raws = data.contents.rawBits; - //FLAC__EntropyCodingMethod_PartitionedRiceContents - // unsigned * parameters - // unsigned * raw_bits - // unsigned capacity_by_order - var ppart = Module.getValue(p_subframe_data + offset, 'i32'); - var pparams = Module.getValue(ppart, 'i32'); - var praw = Module.getValue(ppart + 4, 'i32'); - data.contents.capacityByOrder = Module.getValue(ppart + 8, 'i32'); - for(var i=0; i < partitions; ++i){ - params.push(Module.getValue(pparams + (i*4), 'i32')); - raws.push(Module.getValue(praw + (i*4), 'i32')); - } - offset += 4; - - //FLAC__Subframe_Fixed: - // unsigned order - var order = Module.getValue(p_subframe_data + offset, 'i32'); - offset += 4; - - var warmup = [], res; - - if(is_lpc){ - //FLAC__Subframe_LPC - - // unsigned qlp_coeff_precision - var qlp_coeff_precision = Module.getValue(p_subframe_data + offset, 'i32'); - offset += 4; - // int quantization_level - var quantization_level = Module.getValue(p_subframe_data + offset, 'i32'); - offset += 4; - - //FLAC__Subframe_LPC : - // FLAC__int32 qlp_coeff [FLAC__MAX_LPC_ORDER] - var qlp_coeff = []; - for(var i=0; i < order; ++i){ - qlp_coeff.push(Module.getValue(p_subframe_data + offset, 'i32')); - offset += 4; - } - data.qlp_coeff = qlp_coeff; - data.qlp_coeff_precision = qlp_coeff_precision; - data.quantization_level = quantization_level; - - //FLAC__Subframe_LPC: - // FLAC__int32 warmup [FLAC__MAX_LPC_ORDER] - offset = subOffset.offset + 152; - offset = _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order); - - //FLAC__Subframe_LPC: - // const FLAC__int32 * residual - if(enc_opt && enc_opt.analyseResiduals){ - offset = subOffset.offset + 280; - res = _readSubFrameHdrResidual(p_subframe_data + offset, block_size, order); - } - - } else { - - //FLAC__Subframe_Fixed: - // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER] - offset = _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order); - - //FLAC__Subframe_Fixed: - // const FLAC__int32 * residual - offset = subOffset.offset + 32; - if(enc_opt && enc_opt.analyseResiduals){ - res = _readSubFrameHdrResidual(p_subframe_data + offset, block_size, order); - } - } - - subOffset.offset += 284; - return { - partition: { - type: entropyType, - data: data - }, - order: order, - warmup: warmup, - residual: res - } -} - - -function _readSubFrameHdrWarmup(p_subframe_data, offset, warmup, order){ - - // FLAC__int32 warmup [FLAC__MAX_FIXED_ORDER | FLAC__MAX_LPC_ORDER] - for(var i=0; i < order; ++i){ - warmup.push(Module.getValue(p_subframe_data + offset, 'i32')); - offset += 4; - } - return offset; -} - - -function _readSubFrameHdrResidual(p_subframe_data_res, block_size, order){ - // const FLAC__int32 * residual - var pres = Module.getValue(p_subframe_data_res, 'i32'); - var res = [];//Module.getValue(pres, 'i32'); - //TODO read residual all values(?) - // -> "The residual signal, length == (blocksize minus order) samples. - for(var i=0, size = block_size - order; i < size; ++i){ - res.push(Module.getValue(pres + (i*4), 'i32')); - } - return res; -} - -function _readConstChar(ptr, length, sb){ - sb.splice(0); - var ch; - for(var i=0; i < length; ++i){ - ch = Module.getValue(ptr + i,'i8'); - if(ch === 0){ - break; - } - sb.push(String.fromCodePoint(ch)); - } - return sb.join(''); -} - -function _readNullTerminatedChar(ptr, sb){ - sb.splice(0); - var ch = 1, i = 0; - while(ch > 0){ - ch = Module.getValue(ptr + i++, 'i8'); - if(ch === 0){ - break; - } - sb.push(String.fromCodePoint(ch)); - } - return sb.join(''); -} - - -/** - * HELPER read/extract padding metadata meta-data from meta-data block - * @param {POINTER} p_padding_metadata - * @returns PaddingMetadata - */ -function _readPaddingMetadata(p_padding_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_PADDING (1) - - //FLAC__StreamMetadata_Padding: - // int dummy - return { - dummy: Module.getValue(p_padding_metadata,'i32') - } -} - -/** - * HELPER read/extract application metadata meta-data from meta-data block - * @param {POINTER} p_application_metadata - * @returns ApplicationMetadata - */ -function _readApplicationMetadata(p_application_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_APPLICATION (2) - - //FLAC__StreamMetadata_Application: - // FLAC__byte id [4] - // FLAC__byte * data - return { - id : Module.getValue(p_application_metadata,'i32'), - data: Module.getValue(p_application_metadata + 4,'i32')//TODO should read (binary) data? - } -} - - -/** - * HELPER read/extract seek table metadata meta-data from meta-data block - * @param {POINTER} p_seek_table_metadata - * @returns SeekTableMetadata - */ -function _readSeekTableMetadata(p_seek_table_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_SEEKTABLE (3) - - //FLAC__StreamMetadata_SeekTable: - // unsigned num_points - // FLAC__StreamMetadata_SeekPoint * points - - var num_points = Module.getValue(p_seek_table_metadata,'i32'); - - var ptrPoints = Module.getValue(p_seek_table_metadata + 4,'i32'); - var points = []; - for(var i=0; i < num_points; ++i){ - - //FLAC__StreamMetadata_SeekPoint: - // FLAC__uint64 sample_number - // FLAC__uint64 stream_offset - // unsigned frame_samples - - points.push({ - sample_number: Module.getValue(ptrPoints + (i * 24),'i64'), - stream_offset: Module.getValue(ptrPoints + (i * 24) + 8,'i64'), - frame_samples: Module.getValue(ptrPoints + (i * 24) + 16,'i32') - }); - } - - return { - num_points: num_points, - points: points - } -} - -/** - * HELPER read/extract vorbis comment meta-data from meta-data block - * @param {POINTER} p_vorbiscomment - * @returns VorbisComment - */ -function _readVorbisComment(p_vorbiscomment){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_VORBIS_COMMENT (4) - - // FLAC__StreamMetadata_VorbisComment - // FLAC__StreamMetadata_VorbisComment_Entry vendor_string: - // FLAC__uint32 length - // FLAC__byte * entry - var length = Module.getValue(p_vorbiscomment,'i32'); - var entry = Module.getValue(p_vorbiscomment + 4,'i32'); - - var sb = []; - var strEntry = _readConstChar(entry, length, sb); - - // FLAC__uint32 num_comments - var num_comments = Module.getValue(p_vorbiscomment + 8,'i32'); - - // FLAC__StreamMetadata_VorbisComment_Entry * comments - var comments = [], clen, centry; - var pc = Module.getValue(p_vorbiscomment + 12, 'i32') - for(var i=0; i < num_comments; ++i){ - - // FLAC__StreamMetadata_VorbisComment_Entry - // FLAC__uint32 length - // FLAC__byte * entry - - clen = Module.getValue(pc + (i*8), 'i32'); - if(clen === 0){ - continue; - } - - centry = Module.getValue(pc + (i*8) + 4, 'i32'); - comments.push(_readConstChar(centry, clen, sb)); - } - - return { - vendor_string: strEntry, - num_comments: num_comments, - comments: comments - } -} - -/** - * HELPER read/extract cue sheet meta-data from meta-data block - * @param {POINTER} p_cue_sheet - * @returns CueSheetMetadata - */ -function _readCueSheetMetadata(p_cue_sheet){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_CUESHEET (5) - - // char media_catalog_number [129] - // FLAC__uint64 lead_in - // FLAC__bool is_cd - // unsigned num_tracks - // FLAC__StreamMetadata_CueSheet_Track * tracks - - var sb = []; - var media_catalog_number = _readConstChar(p_cue_sheet, 129, sb); - - var lead_in = Module.getValue(p_cue_sheet + 136,'i64'); - - var is_cd = Module.getValue(p_cue_sheet + 144,'i8'); - var num_tracks = Module.getValue(p_cue_sheet + 148,'i32'); - - var ptrTrack = Module.getValue(p_cue_sheet + 152,'i32'); - var tracks = [], trackOffset = ptrTrack; - if(ptrTrack !== 0){ - - for(var i=0; i < num_tracks; ++i){ - - var tr = _readCueSheetMetadata_track(trackOffset, sb); - tracks.push(tr); - trackOffset += 32; - } - } - - return { - media_catalog_number: media_catalog_number, - lead_in: lead_in, - is_cd: is_cd, - num_tracks: num_tracks, - tracks: tracks - } -} - -/** - * helper read track data for cue-sheet metadata - * @param {POINTER} p_cue_sheet_track pointer to the track data - * @param {string[]} sb "string buffer" temporary buffer for reading string (may be reset) - * @return {CueSheetTrack} - */ -function _readCueSheetMetadata_track(p_cue_sheet_track, sb){ - - // FLAC__StreamMetadata_CueSheet_Track: - // FLAC__uint64 offset - // FLAC__byte number - // char isrc [13] - // unsigned type:1 - // unsigned pre_emphasis:1 - // FLAC__byte num_indices - // FLAC__StreamMetadata_CueSheet_Index * indices - - var typePremph = Module.getValue(p_cue_sheet_track + 22,'i8'); - var num_indices = Module.getValue(p_cue_sheet_track + 23,'i8'); - - var indices = []; - var track = { - offset: Module.getValue(p_cue_sheet_track,'i64'), - number: Module.getValue(p_cue_sheet_track + 8,'i8') &255, - isrc: _readConstChar(p_cue_sheet_track + 9, 13, sb), - type: typePremph & 1? 'NON_AUDIO' : 'AUDIO', - pre_emphasis: !!(typePremph & 2), - num_indices: num_indices, - indices: indices - } - - var idx; - if(num_indices > 0){ - idx = Module.getValue(p_cue_sheet_track + 24,'i32'); - - //FLAC__StreamMetadata_CueSheet_Index: - // FLAC__uint64 offset - // FLAC__byte number - - for(var i=0; i < num_indices; ++i){ - indices.push({ - offset: Module.getValue(idx + (i*16),'i64'), - number: Module.getValue(idx + (i*16) + 8,'i8') - }); - } - } - - return track; -} - -/** - * HELPER read/extract picture meta-data from meta-data block - * @param {POINTER} p_picture_metadata - * @returns PictureMetadata - */ -function _readPictureMetadata(p_picture_metadata){//-> FLAC__StreamMetadata.type (FLAC__MetadataType) === FLAC__METADATA_TYPE_PICTURE (6) - - // FLAC__StreamMetadata_Picture_Type type - // char * mime_type - // FLAC__byte * description - // FLAC__uint32 width - // FLAC__uint32 height - // FLAC__uint32 depth - // FLAC__uint32 colors - // FLAC__uint32 data_length - // FLAC__byte * data - - var type = Module.getValue(p_picture_metadata,'i32'); - - var mime = Module.getValue(p_picture_metadata + 4,'i32'); - - var sb = []; - var mime_type = _readNullTerminatedChar(mime, sb); - - var desc = Module.getValue(p_picture_metadata + 8,'i32'); - var description = _readNullTerminatedChar(desc, sb); - - var width = Module.getValue(p_picture_metadata + 12,'i32'); - var height = Module.getValue(p_picture_metadata + 16,'i32'); - var depth = Module.getValue(p_picture_metadata + 20,'i32'); - var colors = Module.getValue(p_picture_metadata + 24,'i32'); - var data_length = Module.getValue(p_picture_metadata + 28,'i32'); - - var data = Module.getValue(p_picture_metadata + 32,'i32'); - - var buffer = Uint8Array.from(Module.HEAPU8.subarray(data, data + data_length)); - - return { - type: type, - mime_type: mime_type, - description: description, - width: width, - height: height, - depth: depth, - colors: colors, - data_length: data_length, - data: buffer - } -} - -/** - * HELPER workaround / fix for returned write-buffer when decoding FLAC - * - * @param {number} heapOffset - * the offset for the data on HEAPU8 - * @param {Uint8Array} newBuffer - * the target buffer into which the data should be written -- with the correct (block) size - * @param {boolean} applyFix - * whether or not to apply the data repair heuristics - * (handling duplicated/triplicated values in raw data) - */ -function __fix_write_buffer(heapOffset, newBuffer, applyFix){ - - var dv = new DataView(newBuffer.buffer); - var targetSize = newBuffer.length; - - var increase = !applyFix? 1 : 2;//<- for FIX/workaround, NOTE: e.g. if 24-bit padding occurres, there is no fix/increase needed (more details comment below) - var buffer = HEAPU8.subarray(heapOffset, heapOffset + targetSize * increase); - - // FIXME for some reason, the bytes values 0 (min) and 255 (max) get "triplicated", - // or inserted "doubled" which should be ignored, i.e. - // x x x -> x - // x x -> - // where x is 0 or 255 - // -> HACK for now: remove/"over-read" 2 of the values, for each of these triplets/doublications - var jump, isPrint; - for(var i=0, j=0, size = buffer.length; i < size && j < targetSize; ++i, ++j){ - - if(i === size-1 && j < targetSize - 1){ - //increase heap-view, in order to read more (valid) data into the target buffer - buffer = HEAPU8.subarray(heapOffset, size + targetSize); - size = buffer.length; - } - - // NOTE if e.g. 24-bit padding occurres, there does not seem to be no duplication/triplication of 255 or 0, so must not try to fix! - if(applyFix && (buffer[i] === 0 || buffer[i] === 255)){ - - jump = 0; - isPrint = true; - - if(i + 1 < size && buffer[i] === buffer[i+1]){ - - ++jump; - - if(i + 2 < size){ - if(buffer[i] === buffer[i+2]){ - ++jump; - } else { - //if only 2 occurrences: ignore value - isPrint = false; - } - } - }//else: if single value: do print (an do not jump) - - - if(isPrint){ - dv.setUint8(j, buffer[i]); - if(jump === 2 && i + 3 < size && buffer[i] === buffer[i+3]){ - //special case for reducing triples in case the following value is also the same - // (ie. something like: x x x |+ x) - // -> then: do write the value one more time, and jump one further ahead - // i.e. if value occurs 4 times in a row, write 2 values - ++jump; - dv.setUint8(++j, buffer[i]); - } - } else { - --j; - } - - i += jump;//<- apply jump, if there were value duplications - - } else { - dv.setUint8(j, buffer[i]); - } - - } -} - - -// FLAC__STREAM_DECODER_READ_STATUS_CONTINUE The read was OK and decoding can continue. -// FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM The read was attempted while at the end of the stream. Note that the client must only return this value when the read callback was called when already at the end of the stream. Otherwise, if the read itself moves to the end of the stream, the client should still return the data and FLAC__STREAM_DECODER_READ_STATUS_CONTINUE, and then on the next read callback it should return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM with a byte count of 0. -// FLAC__STREAM_DECODER_READ_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. -var FLAC__STREAM_DECODER_READ_STATUS_CONTINUE = 0; -var FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM = 1; -var FLAC__STREAM_DECODER_READ_STATUS_ABORT = 2; - -// FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE The write was OK and decoding can continue. -// FLAC__STREAM_DECODER_WRITE_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. -var FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE = 0; -var FLAC__STREAM_DECODER_WRITE_STATUS_ABORT = 1; - -/** - * @interface FLAC__StreamDecoderInitStatus - * @memberOf Flac - * - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_OK"} 0 Initialization was successful. - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_UNSUPPORTED_CONTAINER"} 1 The library was not compiled with support for the given container format. - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS"} 2 A required callback was not supplied. - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_MEMORY_ALLOCATION_ERROR"} 3 An error occurred allocating memory. - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_ERROR_OPENING_FILE"} 4 fopen() failed in FLAC__stream_decoder_init_file() or FLAC__stream_decoder_init_ogg_file(). - * @property {"FLAC__STREAM_DECODER_INIT_STATUS_ALREADY_INITIALIZED"} 5 FLAC__stream_decoder_init_*() was called when the decoder was already initialized, usually because FLAC__stream_decoder_finish() was not called. - */ -var FLAC__STREAM_DECODER_INIT_STATUS_OK = 0; -var FLAC__STREAM_DECODER_INIT_STATUS_UNSUPPORTED_CONTAINER = 1; -var FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS = 2; -var FLAC__STREAM_DECODER_INIT_STATUS_MEMORY_ALLOCATION_ERROR = 3; -var FLAC__STREAM_DECODER_INIT_STATUS_ERROR_OPENING_FILE = 4; -var FLAC__STREAM_DECODER_INIT_STATUS_ALREADY_INITIALIZED = 5; - -/** - * @interface FLAC__StreamEncoderInitStatus - * @memberOf Flac - * - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_OK"} 0 Initialization was successful. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_ENCODER_ERROR"} 1 General failure to set up encoder; call FLAC__stream_encoder_get_state() for cause. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_UNSUPPORTED_CONTAINER"} 2 The library was not compiled with support for the given container format. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS"} 3 A required callback was not supplied. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_NUMBER_OF_CHANNELS"} 4 The encoder has an invalid setting for number of channels. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BITS_PER_SAMPLE"} 5 The encoder has an invalid setting for bits-per-sample. FLAC supports 4-32 bps but the reference encoder currently supports only up to 24 bps. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE"} 6 The encoder has an invalid setting for the input sample rate. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE"} 7 The encoder has an invalid setting for the block size. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_MAX_LPC_ORDER"} 8 The encoder has an invalid setting for the maximum LPC order. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_QLP_COEFF_PRECISION"} 9 The encoder has an invalid setting for the precision of the quantized linear predictor coefficients. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_BLOCK_SIZE_TOO_SMALL_FOR_LPC_ORDER"} 10 The specified block size is less than the maximum LPC order. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE"} 11 The encoder is bound to the Subset but other settings violate it. - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_METADATA"} 12 The metadata input to the encoder is invalid, in one of the following ways: - * FLAC__stream_encoder_set_metadata() was called with a null pointer but a block count > 0 - * One of the metadata blocks contains an undefined type - * It contains an illegal CUESHEET as checked by FLAC__format_cuesheet_is_legal() - * It contains an illegal SEEKTABLE as checked by FLAC__format_seektable_is_legal() - * It contains more than one SEEKTABLE block or more than one VORBIS_COMMENT block - * @property {"FLAC__STREAM_ENCODER_INIT_STATUS_ALREADY_INITIALIZED"} 13 FLAC__stream_encoder_init_*() was called when the encoder was already initialized, usually because FLAC__stream_encoder_finish() was not called. - */ -var FLAC__STREAM_ENCODER_INIT_STATUS_OK = 0; -var FLAC__STREAM_ENCODER_INIT_STATUS_ENCODER_ERROR = 1; -var FLAC__STREAM_ENCODER_INIT_STATUS_UNSUPPORTED_CONTAINER = 2; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS = 3; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_NUMBER_OF_CHANNELS = 4; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BITS_PER_SAMPLE = 5; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE = 6; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE = 7; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_MAX_LPC_ORDER = 8; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_QLP_COEFF_PRECISION = 9; -var FLAC__STREAM_ENCODER_INIT_STATUS_BLOCK_SIZE_TOO_SMALL_FOR_LPC_ORDER = 10; -var FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE = 11; -var FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_METADATA = 12; -var FLAC__STREAM_ENCODER_INIT_STATUS_ALREADY_INITIALIZED = 13; - -//FLAC__STREAM_ENCODER_WRITE_STATUS_OK The write was OK and encoding can continue. -//FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR An unrecoverable error occurred. The encoder will return from the process call -var FLAC__STREAM_ENCODER_WRITE_STATUS_OK = 0; -var FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR = 1; - - -/** - * Map for encoder/decoder callback functions - * - *
[ID] -> {function_type: FUNCTION}
- * - * type: {[id: number]: {[callback_type: string]: function}} - * @private - */ -var coders = {}; - -/** - * Get a registered callback for the encoder / decoder instance - * - * @param {Number} p_coder - * the encoder/decoder pointer (ID) - * @param {String} func_type - * the callback type, one of - * "write" | "read" | "error" | "metadata" - * @returns {Function} the callback (or VOID if there is no callback registered) - * @private - */ -function getCallback(p_coder, func_type){ - if(coders[p_coder]){ - return coders[p_coder][func_type]; - } -} - -/** - * Register a callback for an encoder / decoder instance (will / should be deleted, when finish()/delete()) - * - * @param {Number} p_coder - * the encoder/decoder pointer (ID) - * @param {String} func_type - * the callback type, one of - * "write" | "read" | "error" | "metadata" - * @param {Function} callback - * the callback function - * @private - */ -function setCallback(p_coder, func_type, callback){ - if(!coders[p_coder]){ - coders[p_coder] = {}; - } - coders[p_coder][func_type] = callback; -} - -/** - * Get coding options for the encoder / decoder instance: - * returns FALSY when not set. - * - * @param {Number} p_coder - * the encoder/decoder pointer (ID) - * @returns {CodingOptions} the coding options - * @private - * @memberOf Flac - */ -function _getOptions(p_coder){ - if(coders[p_coder]){ - return coders[p_coder]["options"]; - } -} - -/** - * Set coding options for an encoder / decoder instance (will / should be deleted, when finish()/delete()) - * - * @param {Number} p_coder - * the encoder/decoder pointer (ID) - * @param {CodingOptions} options - * the coding options - * @private - * @memberOf Flac - */ -function _setOptions(p_coder, options){ - if(!coders[p_coder]){ - coders[p_coder] = {}; - } - coders[p_coder]["options"] = options; -} - -//(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame, void *client_data) -// -> FLAC__StreamEncoderWriteStatus -var enc_write_fn_ptr = addFunction(function(p_encoder, buffer, bytes, samples, current_frame, p_client_data){ - var retdata = new Uint8Array(bytes); - retdata.set(HEAPU8.subarray(buffer, buffer + bytes)); - var write_callback_fn = getCallback(p_encoder, 'write'); - try{ - write_callback_fn(retdata, bytes, samples, current_frame, p_client_data); - } catch(err) { - console.error(err); - return FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; - } - return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; -}, 'iiiiiii'); - -//(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data) -// -> FLAC__StreamDecoderReadStatus -var dec_read_fn_ptr = addFunction(function(p_decoder, buffer, bytes, p_client_data){ - //FLAC__StreamDecoderReadCallback, see https://xiph.org/flac/api/group__flac__stream__decoder.html#ga7a5f593b9bc2d163884348b48c4285fd - - var len = Module.getValue(bytes, 'i32'); - - if(len === 0){ - return FLAC__STREAM_DECODER_READ_STATUS_ABORT; - } - - var read_callback_fn = getCallback(p_decoder, 'read'); - - //callback must return object with: {buffer: TypedArray, readDataLength: number, error: boolean} - var readResult = read_callback_fn(len, p_client_data); - //in case of END_OF_STREAM or an error, readResult.readDataLength must be returned with 0 - - var readLen = readResult.readDataLength; - Module.setValue(bytes, readLen, 'i32'); - - if(readResult.error){ - return FLAC__STREAM_DECODER_READ_STATUS_ABORT; - } - - if(readLen === 0){ - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; - } - - var readBuf = readResult.buffer; - - var dataHeap = new Uint8Array(Module.HEAPU8.buffer, buffer, readLen); - dataHeap.set(new Uint8Array(readBuf)); - - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; -}, 'iiiii'); - -//(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) -// -> FLAC__StreamDecoderWriteStatus -var dec_write_fn_ptr = addFunction(function(p_decoder, p_frame, p_buffer, p_client_data){ - - // var dec = Module.getValue(p_decoder,'i32'); - // var clientData = Module.getValue(p_client_data,'i32'); - - var dec_opts = _getOptions(p_decoder); - var frameInfo = _readFrameHdr(p_frame, dec_opts); - -// console.log(frameInfo);//DEBUG - - var channels = frameInfo.channels; - var block_size = frameInfo.blocksize * (frameInfo.bitsPerSample / 8); - - //whether or not to apply data fixing heuristics (e.g. not needed for 24-bit samples) - var isFix = frameInfo.bitsPerSample !== 24; - - //take padding bits into account for calculating buffer size - // -> seems to be done for uneven byte sizes, i.e. 1 (8 bits) and 3 (24 bits) - var padding = (frameInfo.bitsPerSample / 8)%2; - if(padding > 0){ - block_size += frameInfo.blocksize * padding; - } - - var data = [];//<- array for the data of each channel - var bufferOffset, _buffer; - - for(var i=0; i < channels; ++i){ - - bufferOffset = Module.getValue(p_buffer + (i*4),'i32'); - - _buffer = new Uint8Array(block_size); - //FIXME HACK for "strange" data (see helper function __fix_write_buffer) - __fix_write_buffer(bufferOffset, _buffer, isFix); - - data.push(_buffer.subarray(0, block_size)); - } - - var write_callback_fn = getCallback(p_decoder, 'write'); - var res = write_callback_fn(data, frameInfo);//, clientData); - - // FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE The write was OK and decoding can continue. - // FLAC__STREAM_DECODER_WRITE_STATUS_ABORT An unrecoverable error occurred. The decoder will return from the process call. - - return res !== false? FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE : FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; -}, 'iiiii'); - -/** - * Decoding error codes. - * - *
- * If the error code is not known, value FLAC__STREAM_DECODER_ERROR__UNKNOWN__ is used. - * - * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC"} 0 An error in the stream caused the decoder to lose synchronization. - * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER"} 1 The decoder encountered a corrupted frame header. - * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"} 2 The frame's data did not match the CRC in the footer. - * @property {"FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"} 3 The decoder encountered reserved fields in use in the stream. - * - * - * @interface FLAC__StreamDecoderErrorStatus - * @memberOf Flac - */ -var DecoderErrorCode = { - 0: 'FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC', - 1: 'FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER', - 2: 'FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH', - 3: 'FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM' -} - -//(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) -// -> void -var dec_error_fn_ptr = addFunction(function(p_decoder, err, p_client_data){ - - //err: - var msg = DecoderErrorCode[err] || 'FLAC__STREAM_DECODER_ERROR__UNKNOWN__';//<- this should never happen; - - var error_callback_fn = getCallback(p_decoder, 'error'); - error_callback_fn(err, msg, p_client_data); -}, 'viii'); - -//(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) -> void -//(const FLAC__StreamEncoder *encoder, const FLAC__StreamMetadata *metadata, void *client_data) -> void -var metadata_fn_ptr = addFunction(function(p_coder, p_metadata, p_client_data){ - /* - typedef struct { - FLAC__MetadataType type; - FLAC__bool is_last; - unsigned length; - union { - FLAC__StreamMetadata_StreamInfo stream_info; - FLAC__StreamMetadata_Padding padding; - FLAC__StreamMetadata_Application application; - FLAC__StreamMetadata_SeekTable seek_table; - FLAC__StreamMetadata_VorbisComment vorbis_comment; - FLAC__StreamMetadata_CueSheet cue_sheet; - FLAC__StreamMetadata_Picture picture; - FLAC__StreamMetadata_Unknown unknown; - } data; - } FLAC__StreamMetadata; - */ - - /* - FLAC__METADATA_TYPE_STREAMINFO STREAMINFO block - FLAC__METADATA_TYPE_PADDING PADDING block - FLAC__METADATA_TYPE_APPLICATION APPLICATION block - FLAC__METADATA_TYPE_SEEKTABLE SEEKTABLE block - FLAC__METADATA_TYPE_VORBIS_COMMENT VORBISCOMMENT block (a.k.a. FLAC tags) - FLAC__METADATA_TYPE_CUESHEET CUESHEET block - FLAC__METADATA_TYPE_PICTURE PICTURE block - FLAC__METADATA_TYPE_UNDEFINED marker to denote beginning of undefined type range; this number will increase as new metadata types are added - FLAC__MAX_METADATA_TYPE No type will ever be greater than this. There is not enough room in the protocol block. - */ - - var type = Module.getValue(p_metadata,'i32');//4 bytes - var is_last = Module.getValue(p_metadata+4,'i32');//4 bytes - var length = Module.getValue(p_metadata+8,'i64');//8 bytes - - var meta_data = { - type: type, - isLast: is_last, - length: length, - data: void(0) - }; - - var metadata_callback_fn = getCallback(p_coder, 'metadata'); - if(type === 0){// === FLAC__METADATA_TYPE_STREAMINFO - - meta_data.data = _readStreamInfo(p_metadata+16); - metadata_callback_fn(meta_data.data, meta_data); - - } else { - - var data; - switch(type){ - case 1: //FLAC__METADATA_TYPE_PADDING - data = _readPaddingMetadata(p_metadata+16); - break; - case 2: //FLAC__METADATA_TYPE_APPLICATION - data = readApplicationMetadata(p_metadata+16); - break; - case 3: //FLAC__METADATA_TYPE_SEEKTABLE - data = _readSeekTableMetadata(p_metadata+16); - break; - - case 4: //FLAC__METADATA_TYPE_VORBIS_COMMENT - data = _readVorbisComment(p_metadata+16); - break; - - case 5: //FLAC__METADATA_TYPE_CUESHEET - data = _readCueSheetMetadata(p_metadata+16); - break; - - case 6: //FLAC__METADATA_TYPE_PICTURE - data = _readPictureMetadata(p_metadata+16); - break; - default: { //NOTE this should not happen, and the raw data is very likely not correct! - var cod_opts = _getOptions(p_coder); - if(cod_opts && cod_opts.enableRawMetadata){ - var buffer = Uint8Array.from(HEAPU8.subarray(p_metadata+16, p_metadata+16+length)); - meta_data.raw = buffer; - } - } - - } - - meta_data.data = data; - metadata_callback_fn(void(0), meta_data); - } - -}, 'viii'); - - -////////////// helper fields and functions for event handling -// see exported on()/off() functions -var listeners = {}; -var persistedEvents = []; -var add_event_listener = function (eventName, listener){ - var list = listeners[eventName]; - if(!list){ - list = [listener]; - listeners[eventName] = list; - } else { - list.push(listener); - } - check_and_trigger_persisted_event(eventName, listener); -}; -var check_and_trigger_persisted_event = function(eventName, listener){ - var activated; - for(var i=persistedEvents.length-1; i >= 0; --i){ - activated = persistedEvents[i]; - if(activated && activated.event === eventName){ - listener.apply(null, activated.args); - break; - } - } -}; -var remove_event_listener = function (eventName, listener){ - var list = listeners[eventName]; - if(list){ - for(var i=list.length-1; i >= 0; --i){ - if(list[i] === listener){ - list.splice(i, 1); - } - } - } -}; -/** - * HELPER: fire an event - * @param {string} eventName - * the event name - * @param {any[]} [args] OPITIONAL - * the arguments when triggering the listeners - * @param {boolean} [isPersist] OPTIONAL (positinal argument!) - * if TRUE, handlers for this event that will be registered after this will get triggered immediately - * (i.e. event is "persistent": once triggered it stays "active") - * - */ -var do_fire_event = function (eventName, args, isPersist){ - if(_exported['on'+eventName]){ - _exported['on'+eventName].apply(null, args); - } - var list = listeners[eventName]; - if(list){ - for(var i=0, size=list.length; i < size; ++i){ - list[i].apply(null, args) - } - } - if(isPersist){ - persistedEvents.push({event: eventName, args: args}); - } -} - -///////////////////////////////////// export / public: ///////////////////////////////////////////// -/** - * The Flac module that provides functionality - * for encoding WAV/PCM audio to Flac and decoding Flac to PCM. - * - *

- *

- * NOTE most functions are named analogous to the original C library functions, - * so that its documentation may be used for further reading. - *

- * - * @see https://xiph.org/flac/api/group__flac__stream__encoder.html - * @see https://xiph.org/flac/api/group__flac__stream__decoder.html - * - * @class Flac - * @namespace Flac - */ -var _exported = { - _module: Module,//internal: reference to Flac module - _clear_enc_cb: function(enc_ptr){//internal function: remove reference to encoder instance and its callbacks - delete coders[enc_ptr]; - }, - _clear_dec_cb: function(dec_ptr){//internal function: remove reference to decoder instance and its callbacks - delete coders[dec_ptr]; - }, - /** - * Additional options for encoding or decoding - * @interface CodingOptions - * @memberOf Flac - * @property {boolean} [analyseSubframes] for decoding: include subframes metadata in write-callback metadata, DEFAULT: false - * @property {boolean} [analyseResiduals] for decoding: include residual data in subframes metadata in write-callback metadata, NOTE {@link #analyseSubframes} muste also be enabled, DEFAULT: false - * @property {boolean} [enableRawMetadata] DEBUG option for decoding: enable receiving raw metadata for unknown metadata types in second argument in the metadata-callback, DEFAULT: false - * - * @see Flac#setOptions - * @see Flac~metadata_callback_fn - * @see Flac#FLAC__stream_decoder_set_metadata_respond_all - */ - /** - * FLAC raw metadata - * - * @interface MetadataBlock - * @memberOf Flac - * @property {Flac.FLAC__MetadataType} type the type of the metadata - * @property {boolean} isLast if it is the last block of metadata - * @property {number} length the length of the metadata block (bytes) - * @property {Flac.StreamMetadata | Flac.PaddingMetadata | Flac.ApplicationMetadata | Flac.SeekTableMetadata | Flac.CueSheetMetadata | Flac.PictureMetadata} [data] the metadata (omitted for unknown metadata types) - * @property {Uint8Array} [raw] raw metadata (for debugging: enable via {@link Flac#setOptions}) - */ - /** - * FLAC padding metadata block - * - * @interface PaddingMetadata - * @memberOf Flac - * @property {number} dummy Conceptually this is an empty struct since we don't store the padding bytes. Empty structs are not allowed by some C compilers, hence the dummy. - * - * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_PADDING - */ - /** - * FLAC application metadata block - * - * NOTE the application meta data type is not really supported, i.e. the - * (binary) data is only a pointer to the memory heap. - * - * @interface ApplicationMetadata - * @memberOf Flac - * @property {number} id the application ID - * @property {number} data (pointer) - * - * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_APPLICATION - * @see application block format specification - */ - /** - * FLAC seek table metadata block - * - *

- * From the format specification: - * - * The seek points must be sorted by ascending sample number. - * - * Each seek point's sample number must be the first sample of the target frame. - * - * Each seek point's sample number must be unique within the table - * - * Existence of a SEEKTABLE block implies a correct setting of total_samples in the stream_info block. - * - * Behavior is undefined when more than one SEEKTABLE block is present in a stream. - * - * @interface SeekTableMetadata - * @memberOf Flac - * @property {number} num_points the number of seek points - * @property {Flac.SeekPoint[]} points the seek points - * - * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_SEEKTABLE - */ - /** - * FLAC seek point data - * - * @interface SeekPoint - * @memberOf Flac - * @property {number} sample_number The sample number of the target frame. NOTE -1 for a placeholder point. - * @property {number} stream_offset The offset, in bytes, of the target frame with respect to beginning of the first frame. - * @property {number} frame_samples The number of samples in the target frame. - * - * @see Flac.SeekTableMetadata - */ - /** - * FLAC vorbis comment metadata block - * - * @interface VorbisCommentMetadata - * @memberOf Flac - * @property {string} vendor_string the vendor string - * @property {number} num_comments the number of comments - * @property {string[]} comments the comments - * - * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_VORBIS_COMMENT - */ - /** - * FLAC cue sheet metadata block - * - * @interface CueSheetMetadata - * @memberOf Flac - * @property {string} media_catalog_number Media catalog number, in ASCII printable characters 0x20-0x7e. In general, the media catalog number may be 0 to 128 bytes long. - * @property {number} lead_in The number of lead-in samples. - * @property {boolean} is_cd true if CUESHEET corresponds to a Compact Disc, else false. - * @property {number} num_tracks The number of tracks. - * @property {Flac.CueSheetTrack[]} tracks the tracks - * - * @see Flac.FLAC__MetadataType#FLAC__METADATA_TYPE_CUESHEET - */ - /** - * FLAC cue sheet track data - * - * @interface CueSheetTrack - * @memberOf Flac - * @property {number} offset Track offset in samples, relative to the beginning of the FLAC audio stream. - * @property {number} number The track number. - * @property {string} isrc Track ISRC. This is a 12-digit alphanumeric code. - * @property {"AUDIO" | "NON_AUDIO"} type The track type: audio or non-audio. - * @property {boolean} pre_emphasis The pre-emphasis flag - * @property {number} num_indices The number of track index points. - * @property {Flac.CueSheetTracIndex} indices The track index points. - * - * @see Flac.CueSheetMetadata - */ - /** - * FLAC track index data for cue sheet metadata - * - * @interface CueSheetTracIndex - * @memberOf Flac - * @property {number} offset Offset in samples, relative to the track offset, of the index point. - * @property {number} number The index point number. - * - * @see Flac.CueSheetTrack - */ - /** - * FLAC picture metadata block - * - * @interface PictureMetadata - * @memberOf Flac - * @property {Flac.FLAC__StreamMetadata_Picture_Type} type The kind of picture stored. - * @property {string} mime_type Picture data's MIME type, in ASCII printable characters 0x20-0x7e, NUL terminated. For best compatibility with players, use picture data of MIME type image/jpeg or image/png. A MIME type of '–>' is also allowed, in which case the picture data should be a complete URL. - * @property {string} description Picture's description. - * @property {number} width Picture's width in pixels. - * @property {number} height Picture's height in pixels. - * @property {number} depth Picture's color depth in bits-per-pixel. - * @property {number} colors For indexed palettes (like GIF), picture's number of colors (the number of palette entries), or 0 for non-indexed (i.e. 2^depth). - * @property {number} data_length Length of binary picture data in bytes. - * @property {Uint8Array} data Binary picture data. - */ - /** - * An enumeration of the PICTURE types (see FLAC__StreamMetadataPicture and id3 v2.4 APIC tag). - * - * @interface FLAC__StreamMetadata_Picture_Type - * @memberOf Flac - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_OTHER"} 0 Other - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON_STANDARD"} 1 32x32 pixels 'file icon' (PNG only) - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON"} 2 Other file icon - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FRONT_COVER"} 3 Cover (front) - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BACK_COVER"} 4 Cover (back) - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LEAFLET_PAGE"} 5 Leaflet page - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_MEDIA"} 6 Media (e.g. label side of CD) - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LEAD_ARTIST"} 7 Lead artist/lead performer/soloist - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_ARTIST"} 8 Artist/performer - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_CONDUCTOR"} 9 Conductor - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BAND"} 10 Band/Orchestra - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_COMPOSER"} 11 Composer - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_LYRICIST"} 12 Lyricist/text writer - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_RECORDING_LOCATION"} 13 Recording Location - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_RECORDING"} 14 During recording - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_PERFORMANCE"} 15 During performance - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_VIDEO_SCREEN_CAPTURE"} 16 Movie/video screen capture - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_FISH"} 17 A bright coloured fish - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_ILLUSTRATION"} 18 Illustration - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_BAND_LOGOTYPE"} 19 Band/artist logotype - * @property {"FLAC__STREAM_METADATA_PICTURE_TYPE_PUBLISHER_LOGOTYPE"} 20 Publisher/Studio logotype - * - * @see Flac.PictureMetadata - */ - - /** - * An enumeration of the available metadata block types. - * - * @interface FLAC__MetadataType - * @memberOf Flac - * - * @property {"FLAC__METADATA_TYPE_STREAMINFO"}  0 STREAMINFO block - * @property {"FLAC__METADATA_TYPE_PADDING"}  1 PADDING block - * @property {"FLAC__METADATA_TYPE_APPLICATION"}  2 APPLICATION block - * @property {"FLAC__METADATA_TYPE_SEEKTABLE"}  3 SEEKTABLE block - * @property {"FLAC__METADATA_TYPE_VORBIS_COMMENT"}  4 VORBISCOMMENT block (a.k.a. FLAC tags) - * @property {"FLAC__METADATA_TYPE_CUESHEET"}  5 CUESHEET block - * @property {"FLAC__METADATA_TYPE_PICTURE"}  6 PICTURE block - * @property {"FLAC__METADATA_TYPE_UNDEFINED"}  7 marker to denote beginning of undefined type range; this number will increase as new metadata types are added - * @property {"FLAC__MAX_METADATA_TYPE"}  126 No type will ever be greater than this. There is not enough room in the protocol block. - * - * @see Flac.MetadataBlock - * @see FLAC format documentation - */ - /** - * @function - * @public - * @memberOf Flac# - * @copydoc Flac._setOptions - */ - setOptions: _setOptions, - /** - * @function - * @public - * @memberOf Flac# - * @copydoc Flac._getOptions - */ - getOptions: _getOptions, - /** - * Returns if Flac has been initialized / is ready to be used. - * - * @returns {boolean} true, if Flac is ready to be used - * - * @memberOf Flac# - * @function - * @see #onready - * @see #on - */ - isReady: function() { return _flac_ready; }, - /** - * Hook for handler function that gets called, when asynchronous initialization has finished. - * - * NOTE that if the execution environment does not support Object#defineProperty, then - * this function is not called, after {@link #isReady} is true. - * In this case, {@link #isReady} should be checked, before setting onready - * and if it is true, handler should be executed immediately instead of setting onready. - * - * @memberOf Flac# - * @function - * @param {Flac.event:ReadyEvent} event the ready-event object - * @see #isReady - * @see #on - * @default undefined - * @example - * // [1] if Object.defineProperty() IS supported: - * Flac.onready = function(event){ - * //gets executed when library becomes ready, or immediately, if it already is ready... - * doSomethingWithFlac(); - * }; - * - * // [2] if Object.defineProperty() is NOT supported: - * // do check Flac.isReady(), and only set handler, if not ready yet - * // (otherwise immediately excute handler code) - * if(!Flac.isReady()){ - * Flac.onready = function(event){ - * //gets executed when library becomes ready... - * doSomethingWithFlac(); - * }; - * } else { - * // Flac is already ready: immediately start processing - * doSomethingWithFlac(); - * } - */ - onready: void(0), - /** - * Ready event: is fired when the library has been initialized and is ready to be used - * (e.g. asynchronous loading of binary / WASM modules has been completed). - * - * Before this event is fired, use of functions related to encoding and decoding may - * cause errors. - * - * @event ReadyEvent - * @memberOf Flac - * @type {object} - * @property {"ready"} type the type of the event "ready" - * @property {Flac} target the initalized FLAC library instance - * - * @see #isReady - * @see #on - */ - /** - * Created event: is fired when an encoder or decoder was created. - * - * @event CreatedEvent - * @memberOf Flac - * @type {object} - * @property {"created"} type the type of the event "created" - * @property {Flac.CoderChangedEventData} target the information for the created encoder or decoder - * - * @see #on - */ - /** - * Destroyed event: is fired when an encoder or decoder was destroyed. - * - * @event DestroyedEvent - * @memberOf Flac - * @type {object} - * @property {"destroyed"} type the type of the event "destroyed" - * @property {Flac.CoderChangedEventData} target the information for the destroyed encoder or decoder - * - * @see #on - */ - /** - * Life cycle event data for signaling life cycle changes of encoder or decoder instances - * @interface CoderChangedEventData - * @memberOf Flac - * @property {number} id the ID for the encoder or decoder instance - * @property {"encoder" | "decoder"} type signifies whether the event is for an encoder or decoder instance - * @property {any} [data] specific data for the life cycle change - * - * @see Flac.event:CreatedEvent - * @see Flac.event:DestroyedEvent - */ - /** - * Add an event listener for module-events. - * Supported events: - *

    - *
  • "ready" → {@link Flac.event:ReadyEvent}: emitted when module is ready for usage (i.e. {@link #isReady} is true)
    - * NOTE listener will get immediately triggered if module is already "ready" - *
  • - *
  • "created" → {@link Flac.event:CreatedEvent}: emitted when an encoder or decoder instance was created
    - *
  • - *
  • "destroyed" → {@link Flac.event:DestroyedEvent}: emitted when an encoder or decoder instance was destroyed
    - *
  • - *
- * - * @param {string} eventName - * @param {Function} listener - * - * @memberOf Flac# - * @function - * @see #off - * @see #onready - * @see Flac.event:ReadyEvent - * @see Flac.event:CreatedEvent - * @see Flac.event:DestroyedEvent - * @example - * Flac.on('ready', function(event){ - * //gets executed when library is ready, or becomes ready... - * }); - */ - on: add_event_listener, - /** - * Remove an event listener for module-events. - * @param {string} eventName - * @param {Function} listener - * - * @memberOf Flac# - * @function - * @see #on - */ - off: remove_event_listener, - - /** - * Set the "verify" flag. If true, the encoder will verify it's own encoded output by feeding it through an internal decoder and comparing the original signal against the decoded signal. If a mismatch occurs, the process call will return false. Note that this will slow the encoding process by the extra time required for decoding and comparison. - * - *

- * NOTE: only use on un-initilized encoder instances! - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {boolean} is_verify enable/disable checksum verification during encoding - * - * @returns {boolean} false if the encoder is already initialized, else true - * - * @see #create_libflac_encoder - * @see #FLAC__stream_encoder_get_verify - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_set_verify: function(encoder, is_verify){ - is_verify = is_verify? 1 : 0; - Module.ccall('FLAC__stream_encoder_set_verify', 'number', ['number', 'number'], [ encoder, is_verify ]); - }, - /** - * Set the compression level - * - * The compression level is roughly proportional to the amount of effort the encoder expends to compress the file. A higher level usually means more computation but higher compression. The default level is suitable for most applications. - * - * Currently the levels range from 0 (fastest, least compression) to 8 (slowest, most compression). A value larger than 8 will be treated as 8. - * - * - *

- * NOTE: only use on un-initilized encoder instances! - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {Flac.CompressionLevel} compression_level the desired Flac compression level: [0, 8] - * - * @returns {boolean} false if the encoder is already initialized, else true - * - * @see #create_libflac_encoder - * @see Flac.CompressionLevel - * @see FLAC API for FLAC__stream_encoder_set_compression_level() - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_set_compression_level: Module.cwrap('FLAC__stream_encoder_set_compression_level', 'number', [ 'number', 'number' ]), - /** - * Set the blocksize to use while encoding. - * The number of samples to use per frame. Use 0 to let the encoder estimate a blocksize; this is usually best. - * - *

- * NOTE: only use on un-initilized encoder instances! - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {number} block_size the number of samples to use per frame - * - * @returns {boolean} false if the encoder is already initialized, else true - * - * @see #create_libflac_encoder - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_set_blocksize: Module.cwrap('FLAC__stream_encoder_set_blocksize', 'number', [ 'number', 'number']), - - - /** - * Get the state of the verify stream decoder. Useful when the stream encoder state is FLAC__STREAM_ENCODER_VERIFY_DECODER_ERROR. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @returns {Flac.FLAC__StreamDecoderState} the verify stream decoder state - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_get_verify_decoder_state: Module.cwrap('FLAC__stream_encoder_get_verify_decoder_state', 'number', ['number']), - - /** - * Get the "verify" flag for the encoder. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @returns {boolean} the verify flag for the encoder - * - * - * @memberOf Flac# - * @function - * - * @see #FLAC__stream_encoder_set_verify - */ - FLAC__stream_encoder_get_verify: Module.cwrap('FLAC__stream_encoder_get_verify', 'number', ['number']), -/* - -TODO export other encoder API functions?: - -FLAC__bool FLAC__stream_encoder_set_channels (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_bits_per_sample (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_sample_rate (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_do_mid_side_stereo (FLAC__StreamEncoder *encoder, FLAC__bool value) - -FLAC__bool FLAC__stream_encoder_set_loose_mid_side_stereo (FLAC__StreamEncoder *encoder, FLAC__bool value) - -FLAC__bool FLAC__stream_encoder_set_apodization (FLAC__StreamEncoder *encoder, const char *specification) - -FLAC__bool FLAC__stream_encoder_set_max_lpc_order (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_qlp_coeff_precision (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_do_qlp_coeff_prec_search (FLAC__StreamEncoder *encoder, FLAC__bool value) - -FLAC__bool FLAC__stream_encoder_set_do_escape_coding (FLAC__StreamEncoder *encoder, FLAC__bool value) - -FLAC__bool FLAC__stream_encoder_set_do_exhaustive_model_search (FLAC__StreamEncoder *encoder, FLAC__bool value) - -FLAC__bool FLAC__stream_encoder_set_min_residual_partition_order (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_max_residual_partition_order (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_set_rice_parameter_search_dist (FLAC__StreamEncoder *encoder, unsigned value) - -FLAC__bool FLAC__stream_encoder_get_streamable_subset (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_channels (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_bits_per_sample (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_sample_rate (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_blocksize (const FLAC__StreamEncoder *encoder) - -FLAC__bool FLAC__stream_encoder_get_do_mid_side_stereo (const FLAC__StreamEncoder *encoder) - -FLAC__bool FLAC__stream_encoder_get_loose_mid_side_stereo (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_max_lpc_order (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_qlp_coeff_precision (const FLAC__StreamEncoder *encoder) - -FLAC__bool FLAC__stream_encoder_get_do_qlp_coeff_prec_search (const FLAC__StreamEncoder *encoder) - -FLAC__bool FLAC__stream_encoder_get_do_escape_coding (const FLAC__StreamEncoder *encoder) - -FLAC__bool FLAC__stream_encoder_get_do_exhaustive_model_search (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_min_residual_partition_order (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_max_residual_partition_order (const FLAC__StreamEncoder *encoder) - -unsigned FLAC__stream_encoder_get_rice_parameter_search_dist (const FLAC__StreamEncoder *encoder) - -FLAC__uint64 FLAC__stream_encoder_get_total_samples_estimate (const FLAC__StreamEncoder *encoder) - - - -TODO export other decoder API functions?: - - -const char * FLAC__stream_decoder_get_resolved_state_string (const FLAC__StreamDecoder *decoder) - -FLAC__uint64 FLAC__stream_decoder_get_total_samples (const FLAC__StreamDecoder *decoder) - -unsigned FLAC__stream_decoder_get_channels (const FLAC__StreamDecoder *decoder) - -unsigned FLAC__stream_decoder_get_bits_per_sample (const FLAC__StreamDecoder *decoder) - -unsigned FLAC__stream_decoder_get_sample_rate (const FLAC__StreamDecoder *decoder) - -unsigned FLAC__stream_decoder_get_blocksize (const FLAC__StreamDecoder *decoder) - - -FLAC__bool FLAC__stream_decoder_flush (FLAC__StreamDecoder *decoder) - -FLAC__bool FLAC__stream_decoder_skip_single_frame (FLAC__StreamDecoder *decoder) - - */ - - /** - * Set the compression level - * - * The compression level is roughly proportional to the amount of effort the encoder expends to compress the file. A higher level usually means more computation but higher compression. The default level is suitable for most applications. - * - * Currently the levels range from 0 (fastest, least compression) to 8 (slowest, most compression). A value larger than 8 will be treated as 8. - * - * This function automatically calls the following other set functions with appropriate values, so the client does not need to unless it specifically wants to override them: - *

-	 *     FLAC__stream_encoder_set_do_mid_side_stereo()
-	 *     FLAC__stream_encoder_set_loose_mid_side_stereo()
-	 *     FLAC__stream_encoder_set_apodization()
-	 *     FLAC__stream_encoder_set_max_lpc_order()
-	 *     FLAC__stream_encoder_set_qlp_coeff_precision()
-	 *     FLAC__stream_encoder_set_do_qlp_coeff_prec_search()
-	 *     FLAC__stream_encoder_set_do_escape_coding()
-	 *     FLAC__stream_encoder_set_do_exhaustive_model_search()
-	 *     FLAC__stream_encoder_set_min_residual_partition_order()
-	 *     FLAC__stream_encoder_set_max_residual_partition_order()
-	 *     FLAC__stream_encoder_set_rice_parameter_search_dist()
-	 * 
- * The actual values set for each level are: - * | level | do mid-side stereo | loose mid-side stereo | apodization | max lpc order | qlp coeff precision | qlp coeff prec search | escape coding | exhaustive model search | min residual partition order | max residual partition order | rice parameter search dist | - * |--------|---------------------|------------------------|------------------------------------------------|----------------|----------------------|------------------------|----------------|--------------------------|-------------------------------|-------------------------------|------------------------------| - * | 0 | false | false | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | - * | 1 | true | true | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | - * | 2 | true | false | tukey(0.5) | 0 | 0 | false | false | false | 0 | 3 | 0 | - * | 3 | false | false | tukey(0.5) | 6 | 0 | false | false | false | 0 | 4 | 0 | - * | 4 | true | true | tukey(0.5) | 8 | 0 | false | false | false | 0 | 4 | 0 | - * | 5 | true | false | tukey(0.5) | 8 | 0 | false | false | false | 0 | 5 | 0 | - * | 6 | true | false | tukey(0.5);partial_tukey(2) | 8 | 0 | false | false | false | 0 | 6 | 0 | - * | 7 | true | false | tukey(0.5);partial_tukey(2) | 12 | 0 | false | false | false | 0 | 6 | 0 | - * | 8 | true | false | tukey(0.5);partial_tukey(2);punchout_tukey(3) | 12 | 0 | false | false | false | 0 | 6 | 0 | - * - * @interface CompressionLevel - * @memberOf Flac - * - * @property {"FLAC__COMPRESSION_LEVEL_0"} 0 compression level 0 - * @property {"FLAC__COMPRESSION_LEVEL_1"} 1 compression level 1 - * @property {"FLAC__COMPRESSION_LEVEL_2"} 2 compression level 2 - * @property {"FLAC__COMPRESSION_LEVEL_3"} 3 compression level 3 - * @property {"FLAC__COMPRESSION_LEVEL_4"} 4 compression level 4 - * @property {"FLAC__COMPRESSION_LEVEL_5"} 5 compression level 5 - * @property {"FLAC__COMPRESSION_LEVEL_6"} 6 compression level 6 - * @property {"FLAC__COMPRESSION_LEVEL_7"} 7 compression level 7 - * @property {"FLAC__COMPRESSION_LEVEL_8"} 8 compression level 8 - */ - /** - * Create an encoder. - * - * @param {number} sample_rate - * the sample rate of the input PCM data - * @param {number} channels - * the number of channels of the input PCM data - * @param {number} bps - * bits per sample of the input PCM data - * @param {Flac.CompressionLevel} compression_level - * the desired Flac compression level: [0, 8] - * @param {number} [total_samples] OPTIONAL - * the number of total samples of the input PCM data:
- * Sets an estimate of the total samples that will be encoded. - * This is merely an estimate and may be set to 0 if unknown. - * This value will be written to the STREAMINFO block before encoding, - * and can remove the need for the caller to rewrite the value later if - * the value is known before encoding.
- * If specified, the it will be written into metadata of the FLAC header.
- * DEFAULT: 0 (i.e. unknown number of samples) - * @param {boolean} [is_verify] OPTIONAL - * enable/disable checksum verification during encoding
- * DEFAULT: true
- * NOTE: this argument is positional (i.e. total_samples must also be given) - * @param {number} [block_size] OPTIONAL - * the number of samples to use per frame.
- * DEFAULT: 0 (i.e. encoder sets block size automatically) - * NOTE: this argument is positional (i.e. total_samples and is_verify must also be given) - * - * - * @returns {number} the ID of the created encoder instance (or 0, if there was an error) - * - * @memberOf Flac# - * @function - */ - create_libflac_encoder: function(sample_rate, channels, bps, compression_level, total_samples, is_verify, block_size){ - is_verify = typeof is_verify === 'undefined'? 1 : is_verify + 0; - total_samples = typeof total_samples === 'number'? total_samples : 0; - block_size = typeof block_size === 'number'? block_size : 0; - var ok = true; - var encoder = Module.ccall('FLAC__stream_encoder_new', 'number', [ ], [ ]); - ok &= Module.ccall('FLAC__stream_encoder_set_verify', 'number', ['number', 'number'], [ encoder, is_verify ]); - ok &= Module.ccall('FLAC__stream_encoder_set_compression_level', 'number', ['number', 'number'], [ encoder, compression_level ]); - ok &= Module.ccall('FLAC__stream_encoder_set_channels', 'number', ['number', 'number'], [ encoder, channels ]); - ok &= Module.ccall('FLAC__stream_encoder_set_bits_per_sample', 'number', ['number', 'number'], [ encoder, bps ]); - ok &= Module.ccall('FLAC__stream_encoder_set_sample_rate', 'number', ['number', 'number'], [ encoder, sample_rate ]); - ok &= Module.ccall('FLAC__stream_encoder_set_blocksize', 'number', [ 'number', 'number'], [ encoder, block_size ]); - ok &= Module.ccall('FLAC__stream_encoder_set_total_samples_estimate', 'number', ['number', 'number'], [ encoder, total_samples ]); - if (ok){ - do_fire_event('created', [{type: 'created', target: {id: encoder, type: 'encoder'}}], false); - return encoder; - } - return 0; - }, - /** - * @deprecated use {@link #create_libflac_encoder} instead - * @memberOf Flac# - * @function - */ - init_libflac_encoder: function(){ - console.warn('Flac.init_libflac_encoder() is deprecated, use Flac.create_libflac_encoder() instead!'); - return this.create_libflac_encoder.apply(this, arguments); - }, - - /** - * Create a decoder. - * - * @param {boolean} [is_verify] - * enable/disable checksum verification during decoding
- * DEFAULT: true - * - * @returns {number} the ID of the created decoder instance (or 0, if there was an error) - * - * @memberOf Flac# - * @function - */ - create_libflac_decoder: function(is_verify){ - is_verify = typeof is_verify === 'undefined'? 1 : is_verify + 0; - var ok = true; - var decoder = Module.ccall('FLAC__stream_decoder_new', 'number', [ ], [ ]); - ok &= Module.ccall('FLAC__stream_decoder_set_md5_checking', 'number', ['number', 'number'], [ decoder, is_verify ]); - if (ok){ - do_fire_event('created', [{type: 'created', target: {id: decoder, type: 'decoder'}}], false); - return decoder; - } - return 0; - }, - /** - * @deprecated use {@link #create_libflac_decoder} instead - * @memberOf Flac# - * @function - */ - init_libflac_decoder: function(){ - console.warn('Flac.init_libflac_decoder() is deprecated, use Flac.create_libflac_decoder() instead!'); - return this.create_libflac_decoder.apply(this, arguments); - }, - /** - * The callback for writing the encoded FLAC data. - * - * @callback Flac~encoder_write_callback_fn - * @param {Uint8Array} data the encoded FLAC data - * @param {number} numberOfBytes the number of bytes in data - * @param {number} samples the number of samples encoded in data - * @param {number} currentFrame the number of the (current) encoded frame in data - * @returns {void | false} returning false indicates that an - * unrecoverable error occurred and decoding should be aborted - */ - /** - * The callback for the metadata of the encoded/decoded Flac data. - * - * By default, only the STREAMINFO metadata is enabled. - * - * For other metadata types {@link Flac.FLAC__MetadataType} they need to be enabled, - * see e.g. {@link Flac#FLAC__stream_decoder_set_metadata_respond} - * - * @callback Flac~metadata_callback_fn - * @param {Flac.StreamMetadata | undefined} metadata the FLAC meta data, NOTE only STREAMINFO is returned in first argument, for other types use 2nd argument's metadataBlock.data - * @param {Flac.MetadataBlock} metadataBlock the detailed meta data block - * - * @see Flac#init_decoder_stream - * @see Flac#init_encoder_stream - * @see Flac.CodingOptions - * @see Flac#FLAC__stream_decoder_set_metadata_respond_all - */ - /** - * FLAC meta data - * @interface Metadata - * @memberOf Flac - * @property {number} sampleRate the sample rate (Hz) - * @property {number} channels the number of channels - * @property {number} bitsPerSample bits per sample - */ - /** - * FLAC stream meta data - * @interface StreamMetadata - * @memberOf Flac - * @augments Flac.Metadata - * @property {number} min_blocksize the minimal block size (bytes) - * @property {number} max_blocksize the maximal block size (bytes) - * @property {number} min_framesize the minimal frame size (bytes) - * @property {number} max_framesize the maximal frame size (bytes) - * @property {number} total_samples the total number of (encoded/decoded) samples - * @property {string} md5sum the MD5 checksum for the decoded data (if validation is active) - */ - /** - * Initialize the encoder. - * - * @param {number} encoder - * the ID of the encoder instance that has not been initialized (or has been reset) - * - * @param {Flac~encoder_write_callback_fn} write_callback_fn - * the callback for writing the encoded Flac data: - *
write_callback_fn(data: Uint8Array, numberOfBytes: Number, samples: Number, currentFrame: Number)
- * - * @param {Flac~metadata_callback_fn} [metadata_callback_fn] OPTIONAL - * the callback for the metadata of the encoded Flac data: - *
metadata_callback_fn(metadata: StreamMetadata)
- * - * @param {number|boolean} [ogg_serial_number] OPTIONAL - * if number or true is specified, the encoder will be initialized to - * write to an OGG container, see {@link Flac.init_encoder_ogg_stream}: - * true will set a default serial number (1), - * if specified as number, it will be used as the stream's serial number within the ogg container. - * - * @returns {Flac.FLAC__StreamEncoderInitStatus} the encoder status (0 for FLAC__STREAM_ENCODER_INIT_STATUS_OK) - * - * @memberOf Flac# - * @function - */ - init_encoder_stream: function(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ - - var is_ogg = (ogg_serial_number === true); - client_data = client_data|0; - - if(typeof write_callback_fn !== 'function'){ - return FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS; - } - setCallback(encoder, 'write', write_callback_fn); - - var __metadata_callback_fn_ptr = 0; - if(typeof metadata_callback_fn === 'function'){ - setCallback(encoder, 'metadata', metadata_callback_fn); - __metadata_callback_fn_ptr = metadata_fn_ptr; - } - - //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): - // Module.ccall('FLAC__stream_encoder_init_stream' - var func_name = 'FLAC__stream_encoder_init_stream'; - var args_types = ['number', 'number', 'number', 'number', 'number', 'number']; - var args = [ - encoder, - enc_write_fn_ptr, - 0,// FLAC__StreamEncoderSeekCallback - 0,// FLAC__StreamEncoderTellCallback - __metadata_callback_fn_ptr, - client_data - ]; - - if(typeof ogg_serial_number === 'number'){ - - is_ogg = true; - - } else if(is_ogg){//else: set default serial number for stream in OGG container - - //NOTE from FLAC docs: "It is recommended to set a serial number explicitly as the default of '0' may collide with other streams." - ogg_serial_number = 1; - } - - if(is_ogg){ - //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): - // Module.ccall('FLAC__stream_encoder_init_ogg_stream' - func_name = 'FLAC__stream_encoder_init_ogg_stream'; - - //2nd arg: FLAC__StreamEncoderReadCallback ptr -> duplicate first entry & insert at [1] - args.unshift(args[0]); - args[1] = 0;// FLAC__StreamEncoderReadCallback - - args_types.unshift(args_types[0]); - args_types[1] = 'number'; - - - //NOTE ignore BOOL return value when setting serial number, since init-call's returned - // status will also indicate, if encoder already has been initialized - Module.ccall( - 'FLAC__stream_encoder_set_ogg_serial_number', 'number', - ['number', 'number'], - [ encoder, ogg_serial_number ] - ); - } - - var init_status = Module.ccall(func_name, 'number', args_types, args); - - return init_status; - }, - /** - * Initialize the encoder for writing to an OGG container. - * - * @param {number} [ogg_serial_number] OPTIONAL - * the serial number for the stream in the OGG container - * DEFAULT: 1 - * - * @memberOf Flac# - * @function - * @copydoc #init_encoder_stream - */ - init_encoder_ogg_stream: function(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ - - if(typeof ogg_serial_number !== 'number'){ - ogg_serial_number = true; - } - return this.init_encoder_stream(encoder, write_callback_fn, metadata_callback_fn, ogg_serial_number, client_data); - }, - /** - * Result / return value for {@link Flac~decoder_read_callback_fn} callback function - * - * @interface ReadResult - * @memberOf Flac - * @property {TypedArray} buffer a TypedArray (e.g. Uint8Array) with the read data - * @property {number} readDataLength the number of read data bytes. A number of 0 (zero) indicates that the end-of-stream is reached. - * @property {boolean} [error] OPTIONAL value of true indicates that an error occured (decoding will be aborted) - */ - /** - * Result / return value for {@link Flac~decoder_read_callback_fn} callback function for signifying that there is no more data to read - * - * @interface CompletedReadResult - * @memberOf Flac - * @augments Flac.ReadResult - * @property {TypedArray | undefined} buffer a TypedArray (e.g. Uint8Array) with the read data (will be ignored in case readDataLength is 0) - * @property {0} readDataLength the number of read data bytes: The number of 0 (zero) indicates that the end-of-stream is reached. - */ - /** - * The callback for reading the FLAC data that will be decoded. - * - * @callback Flac~decoder_read_callback_fn - * @param {number} numberOfBytes the maximal number of bytes that the read callback can return - * @returns {Flac.ReadResult | Flac.CompletedReadResult} the result of the reading action/request - */ - /** - * The callback for writing the decoded FLAC data. - * - * @callback Flac~decoder_write_callback_fn - * @param {Uint8Array[]} data array of the channels with the decoded PCM data as Uint8Arrays - * @param {Flac.BlockMetadata} frameInfo the metadata information for the decoded data - */ - /** - * The callback for reporting decoding errors. - * - * @callback Flac~decoder_error_callback_fn - * @param {number} errorCode the error code - * @param {Flac.FLAC__StreamDecoderErrorStatus} errorDescription the string representation / description of the error - */ - /** - * FLAC block meta data - * @interface BlockMetadata - * @augments Flac.Metadata - * @memberOf Flac - * - * @property {number} blocksize the block size (bytes) - * @property {number} number the number of the decoded samples or frames - * @property {string} numberType the type to which number refers to: either "frames" or "samples" - * @property {Flac.FLAC__ChannelAssignment} channelAssignment the channel assignment - * @property {string} crc the MD5 checksum for the decoded data, if validation is enabled - * @property {Flac.SubFrameMetadata[]} [subframes] the metadata of the subframes. The array length corresponds to the number of channels. NOTE will only be included if {@link Flac.CodingOptions CodingOptions.analyseSubframes} is enabled for the decoder. - * - * @see Flac.CodingOptions - * @see Flac#setOptions - */ - /** - * FLAC subframe metadata - * @interface SubFrameMetadata - * @memberOf Flac - * - * @property {Flac.FLAC__SubframeType} type the type of the subframe - * @property {number|Flac.FixedSubFrameData|Flac.LPCSubFrameData} data the type specific metadata for subframe - * @property {number} wastedBits the wasted bits-per-sample - */ - /** - * metadata for FIXED subframe type - * @interface FixedSubFrameData - * @memberOf Flac - * - * @property {number} order The polynomial order. - * @property {number[]} warmup Warmup samples to prime the predictor, length == order. - * @property {Flac.SubFramePartition} partition The residual coding method. - * @property {number[]} [residual] The residual signal, length == (blocksize minus order) samples. - * NOTE will only be included if {@link Flac.CodingOptions CodingOptions.analyseSubframes} is enabled for the decoder. - */ - /** - * metadata for LPC subframe type - * @interface LPCSubFrameData - * @augments Flac.FixedSubFrameData - * @memberOf Flac - * - * @property {number} order The FIR order. - * @property {number[]} qlp_coeff FIR filter coefficients. - * @property {number} qlp_coeff_precision Quantized FIR filter coefficient precision in bits. - * @property {number} quantization_level The qlp coeff shift needed. - */ - /** - * metadata for FIXED or LPC subframe partitions - * @interface SubFramePartition - * @memberOf Flac - * - * @property {Flac.FLAC__EntropyCodingMethodType} type the entropy coding method - * @property {Flac.SubFramePartitionData} data metadata for a Rice partitioned residual - */ - /** - * metadata for FIXED or LPC subframe partition data - * @interface SubFramePartitionData - * @memberOf Flac - * - * @property {number} order The partition order, i.e. # of contexts = 2 ^ order. - * @property {Flac.SubFramePartitionContent} contents The context's Rice parameters and/or raw bits. - */ - /** - * metadata for FIXED or LPC subframe partition data content - * @interface SubFramePartitionContent - * @memberOf Flac - * - * @property {number[]} parameters The Rice parameters for each context. - * @property {number[]} rawBits Widths for escape-coded partitions. Will be non-zero for escaped partitions and zero for unescaped partitions. - * @property {number} capacityByOrder The capacity of the parameters and raw_bits arrays specified as an order, i.e. the number of array elements allocated is 2 ^ capacity_by_order. - */ - /** - * The types for FLAC subframes - * - * @interface FLAC__SubframeType - * @memberOf Flac - * - * @property {"FLAC__SUBFRAME_TYPE_CONSTANT"}  0 constant signal - * @property {"FLAC__SUBFRAME_TYPE_VERBATIM"}  1 uncompressed signal - * @property {"FLAC__SUBFRAME_TYPE_FIXED"}  2 fixed polynomial prediction - * @property {"FLAC__SUBFRAME_TYPE_LPC"}  3 linear prediction - */ - /** - * The channel assignment for the (decoded) frame. - * - * @interface FLAC__ChannelAssignment - * @memberOf Flac - * - * @property {"FLAC__CHANNEL_ASSIGNMENT_INDEPENDENT"} 0 independent channels - * @property {"FLAC__CHANNEL_ASSIGNMENT_LEFT_SIDE"} 1 left+side stereo - * @property {"FLAC__CHANNEL_ASSIGNMENT_RIGHT_SIDE"} 2 right+side stereo - * @property {"FLAC__CHANNEL_ASSIGNMENT_MID_SIDE"} 3 mid+side stereo - */ - /** - * entropy coding methods - * - * @interface FLAC__EntropyCodingMethodType - * @memberOf Flac - * - * @property {"FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE"}  0 Residual is coded by partitioning into contexts, each with it's own 4-bit Rice parameter. - * @property {"FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2"}  1 Residual is coded by partitioning into contexts, each with it's own 5-bit Rice parameter. - */ - /** - * Initialize the decoder. - * - * @param {number} decoder - * the ID of the decoder instance that has not been initialized (or has been reset) - * - * @param {Flac~decoder_read_callback_fn} read_callback_fn - * the callback for reading the Flac data that should get decoded: - *
read_callback_fn(numberOfBytes: Number) : {buffer: ArrayBuffer, readDataLength: number, error: boolean}
- * - * @param {Flac~decoder_write_callback_fn} write_callback_fn - * the callback for writing the decoded data: - *
write_callback_fn(data: Uint8Array[], frameInfo: Metadata)
- * - * @param {Flac~decoder_error_callback_fn} error_callback_fn - * the error callback: - *
error_callback_fn(errorCode: Number, errorDescription: String)
- * - * @param {Flac~metadata_callback_fn} [metadata_callback_fn] OPTIONAL - * callback for receiving the metadata of FLAC data that will be decoded: - *
metadata_callback_fn(metadata: StreamMetadata)
- * - * @param {number|boolean} [ogg_serial_number] OPTIONAL - * if number or true is specified, the decoder will be initilized to - * read from an OGG container, see {@link Flac.init_decoder_ogg_stream}:
- * true will use the default serial number, if specified as number the - * corresponding stream with the serial number from the ogg container will be used. - * - * @returns {Flac.FLAC__StreamDecoderInitStatus} the decoder status(0 for FLAC__STREAM_DECODER_INIT_STATUS_OK) - * - * @memberOf Flac# - * @function - */ - init_decoder_stream: function(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ - - client_data = client_data|0; - - if(typeof read_callback_fn !== 'function'){ - return FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS; - } - setCallback(decoder, 'read', read_callback_fn); - - if(typeof write_callback_fn !== 'function'){ - return FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS; - } - setCallback(decoder, 'write', write_callback_fn); - - var __error_callback_fn_ptr = 0; - if(typeof error_callback_fn === 'function'){ - setCallback(decoder, 'error', error_callback_fn); - __error_callback_fn_ptr = dec_error_fn_ptr; - } - - var __metadata_callback_fn_ptr = 0; - if(typeof metadata_callback_fn === 'function'){ - setCallback(decoder, 'metadata', metadata_callback_fn); - __metadata_callback_fn_ptr = metadata_fn_ptr; - } - - var is_ogg = (ogg_serial_number === true); - if(typeof ogg_serial_number === 'number'){ - - is_ogg = true; - - //NOTE ignore BOOL return value when setting serial number, since init-call's returned - // status will also indicate, if decoder already has been initialized - Module.ccall( - 'FLAC__stream_decoder_set_ogg_serial_number', 'number', - ['number', 'number'], - [ decoder, ogg_serial_number ] - ); - } - - //NOTE the following comments are used for auto-detecting exported functions (only change if ccall function name(s) change!): - // Module.ccall('FLAC__stream_decoder_init_stream' - // Module.ccall('FLAC__stream_decoder_init_ogg_stream' - var init_func_name = !is_ogg? 'FLAC__stream_decoder_init_stream' : 'FLAC__stream_decoder_init_ogg_stream'; - - var init_status = Module.ccall( - init_func_name, 'number', - [ 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'], - [ - decoder, - dec_read_fn_ptr, - 0,// FLAC__StreamDecoderSeekCallback - 0,// FLAC__StreamDecoderTellCallback - 0,// FLAC__StreamDecoderLengthCallback - 0,// FLAC__StreamDecoderEofCallback - dec_write_fn_ptr, - __metadata_callback_fn_ptr, - __error_callback_fn_ptr, - client_data - ] - ); - - return init_status; - }, - /** - * Initialize the decoder for writing to an OGG container. - * - * @param {number} [ogg_serial_number] OPTIONAL - * the serial number for the stream in the OGG container that should be decoded.
- * The default behavior is to use the serial number of the first Ogg page. Setting a serial number here will explicitly specify which stream is to be decoded. - * - * @memberOf Flac# - * @function - * @copydoc #init_decoder_stream - */ - init_decoder_ogg_stream: function(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data){ - - if(typeof ogg_serial_number !== 'number'){ - ogg_serial_number = true; - } - return this.init_decoder_stream(decoder, read_callback_fn, write_callback_fn, error_callback_fn, metadata_callback_fn, ogg_serial_number, client_data); - }, - /** - * Encode / submit data for encoding. - * - * This version allows you to supply the input data where the channels are interleaved into a - * single array (i.e. channel0_sample0, channel1_sample0, ... , channelN_sample0, channel0_sample1, ...). - * - * The samples need not be block-aligned but they must be sample-aligned, i.e. the first value should be - * channel0_sample0 and the last value channelN_sampleM. - * - * Each sample should be a signed integer, right-justified to the resolution set by bits-per-sample. - * - * For example, if the resolution is 16 bits per sample, the samples should all be in the range [-32768,32767]. - * - * - * For applications where channel order is important, channels must follow the order as described in the frame header. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {TypedArray} buffer - * the audio data in a typed array with signed integers (and size according to the set bits-per-sample setting) - * - * @param {number} num_of_samples - * the number of samples in buffer - * - * @returns {boolean} true if successful, else false; in this case, check the encoder state with FLAC__stream_encoder_get_state() to see what went wrong. - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_process_interleaved: function(encoder, buffer, num_of_samples){ - // get the length of the data in bytes - var numBytes = buffer.length * buffer.BYTES_PER_ELEMENT; - // malloc enough space for the data - var ptr = Module._malloc(numBytes); - // get a bytes-wise view on the newly allocated buffer - var heapBytes= new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); - // copy data into heapBytes - heapBytes.set(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));// issue #11 (2): do use byteOffset and byteLength for copying the data in case the underlying buffer/ArrayBuffer of the TypedArray view is larger than the TypedArray - var status = Module.ccall('FLAC__stream_encoder_process_interleaved', 'number', - ['number', 'number', 'number'], - [encoder, heapBytes.byteOffset, num_of_samples] - ); - Module._free(ptr); - return status; - }, - - /** - * Encode / submit data for encoding. - * - * Submit data for encoding. This version allows you to supply the input data via an array of pointers, - * each pointer pointing to an array of samples samples representing one channel. - * The samples need not be block-aligned, but each channel should have the same number of samples. - * - * Each sample should be a signed integer, right-justified to the resolution set by FLAC__stream_encoder_set_bits_per_sample(). - * For example, if the resolution is 16 bits per sample, the samples should all be in the range [-32768,32767]. - * - * - * For applications where channel order is important, channels must follow the order as described in the frame header. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {TypedArray[]} channelBuffers - * an array for the audio data channels as typed arrays with signed integers (and size according to the set bits-per-sample setting) - * - * @param {number} num_of_samples - * the number of samples in one channel (i.e. one of the buffers) - * - * @returns {boolean} true if successful, else false; in this case, check the encoder state with FLAC__stream_encoder_get_state() to see what went wrong. - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_process: function(encoder, channelBuffers, num_of_samples){ - - var ptrInfo = this._create_pointer_array(channelBuffers); - var pointerPtr = ptrInfo.pointerPointer; - - var status = Module.ccall('FLAC__stream_encoder_process', 'number', - ['number', 'number', 'number'], - [encoder, pointerPtr, num_of_samples] - ); - - this._destroy_pointer_array(ptrInfo); - return status; - }, - /** - * Decodes a single frame. - * - * To check decoding progress, use {@link #FLAC__stream_decoder_get_state}. - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} FALSE if an error occurred - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_process_single: Module.cwrap('FLAC__stream_decoder_process_single', 'number', ['number']), - - /** - * Decodes data until end of stream. - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} FALSE if an error occurred - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_process_until_end_of_stream: Module.cwrap('FLAC__stream_decoder_process_until_end_of_stream', 'number', ['number']), - - /** - * Decodes data until end of metadata. - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} false if any fatal read, write, or memory allocation error occurred (meaning decoding must stop), else true. - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_process_until_end_of_metadata: Module.cwrap('FLAC__stream_decoder_process_until_end_of_metadata', 'number', ['number']), - - /** - * Decoder state code. - * - * @interface FLAC__StreamDecoderState - * @memberOf Flac - * - * @property {"FLAC__STREAM_DECODER_SEARCH_FOR_METADATA"} 0 The decoder is ready to search for metadata - * @property {"FLAC__STREAM_DECODER_READ_METADATA"} 1 The decoder is ready to or is in the process of reading metadata - * @property {"FLAC__STREAM_DECODER_SEARCH_FOR_FRAME_SYNC"} 2 The decoder is ready to or is in the process of searching for the frame sync code - * @property {"FLAC__STREAM_DECODER_READ_FRAME"} 3 The decoder is ready to or is in the process of reading a frame - * @property {"FLAC__STREAM_DECODER_END_OF_STREAM"} 4 The decoder has reached the end of the stream - * @property {"FLAC__STREAM_DECODER_OGG_ERROR"} 5 An error occurred in the underlying Ogg layer - * @property {"FLAC__STREAM_DECODER_SEEK_ERROR"} 6 An error occurred while seeking. The decoder must be flushed with FLAC__stream_decoder_flush() or reset with FLAC__stream_decoder_reset() before decoding can continue - * @property {"FLAC__STREAM_DECODER_ABORTED"} 7 The decoder was aborted by the read callback - * @property {"FLAC__STREAM_DECODER_MEMORY_ALLOCATION_ERROR"} 8 An error occurred allocating memory. The decoder is in an invalid state and can no longer be used - * @property {"FLAC__STREAM_DECODER_UNINITIALIZED"} 9 The decoder is in the uninitialized state; one of the FLAC__stream_decoder_init_*() functions must be called before samples can be processed. - * - */ - /** - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {Flac.FLAC__StreamDecoderState} the decoder state - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_get_state: Module.cwrap('FLAC__stream_decoder_get_state', 'number', ['number']), - - /** - * Encoder state code. - * - * @interface FLAC__StreamEncoderState - * @memberOf Flac - * - * @property {"FLAC__STREAM_ENCODER_OK"} 0 The encoder is in the normal OK state and samples can be processed. - * @property {"FLAC__STREAM_ENCODER_UNINITIALIZED"} 1 The encoder is in the uninitialized state; one of the FLAC__stream_encoder_init_*() functions must be called before samples can be processed. - * @property {"FLAC__STREAM_ENCODER_OGG_ERROR"} 2 An error occurred in the underlying Ogg layer. - * @property {"FLAC__STREAM_ENCODER_VERIFY_DECODER_ERROR"} 3 An error occurred in the underlying verify stream decoder; check FLAC__stream_encoder_get_verify_decoder_state(). - * @property {"FLAC__STREAM_ENCODER_VERIFY_MISMATCH_IN_AUDIO_DATA"} 4 The verify decoder detected a mismatch between the original audio signal and the decoded audio signal. - * @property {"FLAC__STREAM_ENCODER_CLIENT_ERROR"} 5 One of the callbacks returned a fatal error. - * @property {"FLAC__STREAM_ENCODER_IO_ERROR"} 6 An I/O error occurred while opening/reading/writing a file. Check errno. - * @property {"FLAC__STREAM_ENCODER_FRAMING_ERROR"} 7 An error occurred while writing the stream; usually, the write_callback returned an error. - * @property {"FLAC__STREAM_ENCODER_MEMORY_ALLOCATION_ERROR"} 8 Memory allocation failed. - * - */ - /** - * - * @param {number} encoder - * the ID of the encoder instance - * - * @returns {Flac.FLAC__StreamEncoderState} the encoder state - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_get_state: Module.cwrap('FLAC__stream_encoder_get_state', 'number', ['number']), - /** - * Direct the decoder to pass on all metadata blocks of type type. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @param {Flac.FLAC__MetadataType} type the metadata type to be enabled - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_respond_all - */ - FLAC__stream_decoder_set_metadata_respond: Module.cwrap('FLAC__stream_decoder_set_metadata_respond', 'number', ['number', 'number']), - /** - * Direct the decoder to pass on all APPLICATION metadata blocks of the given id. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @param {number} id the ID of application metadata - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_respond_all - */ - FLAC__stream_decoder_set_metadata_respond_application: Module.cwrap('FLAC__stream_decoder_set_metadata_respond_application', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, const FLAC__byte id[4]) - /** - * Direct the decoder to pass on all metadata blocks of any type. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all - * @see Flac#FLAC__stream_decoder_set_metadata_respond_application - * @see Flac#FLAC__stream_decoder_set_metadata_respond - */ - FLAC__stream_decoder_set_metadata_respond_all: Module.cwrap('FLAC__stream_decoder_set_metadata_respond_all', 'number', ['number']),// (FLAC__StreamDecoder *decoder) - /** - * Direct the decoder to filter out all metadata blocks of type type. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @param {Flac.FLAC__MetadataType} type the metadata type to be ignored - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all - */ - FLAC__stream_decoder_set_metadata_ignore: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, FLAC__MetadataType type) - /** - * Direct the decoder to filter out all APPLICATION metadata blocks of the given id. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @param {number} id the ID of application metadata - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_ignore_all - */ - FLAC__stream_decoder_set_metadata_ignore_application: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore_application', 'number', ['number', 'number']),// (FLAC__StreamDecoder *decoder, const FLAC__byte id[4]) - /** - * Direct the decoder to filter out all metadata blocks of any type. - * - * By default, only the STREAMINFO block is returned via the metadata callback. - * - *

- * NOTE: only use on un-initilized decoder instances! - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#FLAC__stream_decoder_set_metadata_respond_all - * @see Flac#FLAC__stream_decoder_set_metadata_ignore - * @see Flac#FLAC__stream_decoder_set_metadata_ignore_application - */ - FLAC__stream_decoder_set_metadata_ignore_all: Module.cwrap('FLAC__stream_decoder_set_metadata_ignore_all', 'number', ['number']),// (FLAC__StreamDecoder *decoder) - /** - * Set the metadata blocks to be emitted to the stream before encoding. A value of NULL, 0 implies no metadata; otherwise, supply an array of pointers to metadata blocks. - * The array is non-const since the encoder may need to change the is_last flag inside them, and in some cases update seek point offsets. Otherwise, the encoder - * will not modify or free the blocks. It is up to the caller to free the metadata blocks after encoding finishes. - * - *

- * The encoder stores only copies of the pointers in the metadata array; the metadata blocks themselves must survive at least until after FLAC__stream_encoder_finish() returns. - * Do not free the blocks until then. - * - * The STREAMINFO block is always written and no STREAMINFO block may occur in the supplied array. - * - * By default the encoder does not create a SEEKTABLE. If one is supplied in the metadata array, but the client has specified that it does not support seeking, - * then the SEEKTABLE will be written verbatim. However by itself this is not very useful as the client will not know the stream offsets for the seekpoints ahead of time. - * In order to get a proper seektable the client must support seeking. See next note. - * - * SEEKTABLE blocks are handled specially. Since you will not know the values for the seek point stream offsets, you should pass in a SEEKTABLE 'template', that is, - * a SEEKTABLE object with the required sample numbers (or placeholder points), with 0 for the frame_samples and stream_offset fields for each point. - * If the client has specified that it supports seeking by providing a seek callback to FLAC__stream_encoder_init_stream() or both seek AND read callback to - * FLAC__stream_encoder_init_ogg_stream() (or by using FLAC__stream_encoder_init*_file() or FLAC__stream_encoder_init*_FILE()), then while it is encoding the encoder will - * fill the stream offsets in for you and when encoding is finished, it will seek back and write the real values into the SEEKTABLE block in the stream. There are helper - * routines for manipulating seektable template blocks; see metadata.h: FLAC__metadata_object_seektable_template_*(). If the client does not support seeking, - * the SEEKTABLE will have inaccurate offsets which will slow down or remove the ability to seek in the FLAC stream. - * - * The encoder instance will modify the first SEEKTABLE block as it transforms the template to a valid seektable while encoding, but it is still up to the caller to free - * all metadata blocks after encoding. - * - * A VORBIS_COMMENT block may be supplied. The vendor string in it will be ignored. libFLAC will use it's own vendor string. libFLAC will not modify the passed-in - * VORBIS_COMMENT's vendor string, it will simply write it's own into the stream. If no VORBIS_COMMENT block is present in the metadata array, libFLAC will write an - * empty one, containing only the vendor string. - * - * The Ogg FLAC mapping requires that the VORBIS_COMMENT block be the second metadata block of the stream. The encoder already supplies the STREAMINFO block automatically. - * - * If metadata does not contain a VORBIS_COMMENT block, the encoder will supply that too. Otherwise, if metadata does contain a VORBIS_COMMENT block and it is not the first, - * the init function will reorder metadata by moving the VORBIS_COMMENT block to the front; the relative ordering of the other blocks will remain as they were. - * - * The Ogg FLAC mapping limits the number of metadata blocks per stream to 65535. If num_blocks exceeds this the function will return false. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @param {Flac.PointerInfo} metadataBuffersPointer - * - * @param {number} num_blocks - * - * @returns {boolean} false if the encoder is already initialized, else true. false if the encoder is already initialized, or if num_blocks > 65535 if encoding to Ogg FLAC, else true. - * - * @memberOf Flac# - * @function - * - * @see Flac.FLAC__MetadataType - * @see Flac#_create_pointer_array - * @see Flac#_destroy_pointer_array - */ - FLAC__stream_encoder_set_metadata: function(encoder, metadataBuffersPointer, num_blocks){// ( FLAC__StreamEncoder * encoder, FLAC__StreamMetadata ** metadata, unsigned num_blocks) - var status = Module.ccall('FLAC__stream_encoder_set_metadata', 'number', - ['number', 'number', 'number'], - [encoder, metadataBuffersPointer.pointerPointer, num_blocks] - ); - return status; - }, - /** - * Helper object for allocating an array of buffers on the (memory) heap. - * - * @interface PointerInfo - * @memberOf Flac - * @property {number} pointerPointer pointer to the array of (pointer) buffers - * @property {number[]} dataPointer array of pointers to the allocated data arrays (i.e. buffers) - * - * @see Flac#_create_pointer_array - * @see Flac#_destroy_pointer_array - */ - /** - * Helper function for creating pointer (and allocating the data) to an array of buffers on the (memory) heap. - * - * Use the returned PointerInfo.dataPointer as argument, where the array-pointer is required. - * - * NOTE: afer use, the allocated buffers on the heap need be freed, see {@link #_destroy_pointer_array}. - * - * @param {Uint8Array[]} bufferArray - * the buffer for which to create - * - * @returns {Flac.PointerInfo} false if the decoder is already initialized, else true - * - * @memberOf Flac# - * @function - * - * @see Flac#_destroy_pointer_array - */ - _create_pointer_array: function(bufferArray){ - var size=bufferArray.length; - var ptrs = [], ptrData = new Uint32Array(size); - var ptrOffsets = new DataView(ptrData.buffer); - var buffer, numBytes, heapBytes, ptr; - for(var i=0, size; i < size; ++i){ - buffer = bufferArray[i]; - // get the length of the data in bytes - numBytes = buffer.length * buffer.BYTES_PER_ELEMENT; - // malloc enough space for the data - ptr = Module._malloc(numBytes); - ptrs.push(ptr); - // get a bytes-wise view on the newly allocated buffer - heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); - // copy data into heapBytes - heapBytes.set(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));// use FIX for issue #11 (2) - ptrOffsets.setUint32(i*4, ptr, true); - } - var nPointerBytes = ptrData.length * ptrData.BYTES_PER_ELEMENT - var pointerPtr = Module._malloc(nPointerBytes); - var pointerHeap = new Uint8Array(Module.HEAPU8.buffer, pointerPtr, nPointerBytes); - pointerHeap.set( new Uint8Array(ptrData.buffer) ); - - return { - dataPointer: ptrs, - pointerPointer: pointerPtr - }; - }, - /** - * Helper function for destroying/freeing a previously created pointer (and allocating the data) of an array of buffers on the (memory) heap. - * - * @param {Flac.PointerInfo} pointerInfo - * the pointer / allocation information that should be destroyed/freed - * - * - * @memberOf Flac# - * @function - * - * @see Flac#_create_pointer_array - */ - _destroy_pointer_array: function(pointerInfo){ - var pointerArray = pointerInfo.dataPointer; - for(var i=0, size=pointerArray.length; i < size; ++i){ - Module._free(pointerArray[i]); - } - Module._free(pointerInfo.pointerPointer); - }, - /** - * Get if MD5 verification is enabled for the decoder - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} true if MD5 verification is enabled - * - * @memberOf Flac# - * @function - * - * @see #FLAC__stream_decoder_set_md5_checking - */ - FLAC__stream_decoder_get_md5_checking: Module.cwrap('FLAC__stream_decoder_get_md5_checking', 'number', ['number']), - - /** - * Set the "MD5 signature checking" flag. If true, the decoder will compute the MD5 signature of the unencoded audio data while decoding and compare it to the signature from the STREAMINFO block, - * if it exists, during {@link Flac.FLAC__stream_decoder_finish FLAC__stream_decoder_finish()}. - * - * MD5 signature checking will be turned off (until the next {@link Flac.FLAC__stream_decoder_reset FLAC__stream_decoder_reset()}) if there is no signature in the STREAMINFO block or when a seek is attempted. - * - * Clients that do not use the MD5 check should leave this off to speed up decoding. - * - * @param {number} decoder - * the ID of the decoder instance - * @param {boolean} is_verify - * enable/disable checksum verification during decoding - * @returns {boolean} FALSE if the decoder is already initialized, else TRUE. - * - * @memberOf Flac# - * @function - * - * @see #FLAC__stream_decoder_get_md5_checking - */ - FLAC__stream_decoder_set_md5_checking: function(decoder, is_verify){ - is_verify = is_verify? 1 : 0; - return Module.ccall('FLAC__stream_decoder_set_md5_checking', 'number', ['number', 'number'], [ decoder, is_verify ]); - }, - - /** - * Finish the encoding process. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @returns {boolean} false if an error occurred processing the last frame; - * or if verify mode is set, there was a verify mismatch; else true. - * If false, caller should check the state with {@link Flac#FLAC__stream_encoder_get_state} - * for more information about the error. - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_finish: Module.cwrap('FLAC__stream_encoder_finish', 'number', [ 'number' ]), - /** - * Finish the decoding process. - * - * The decoder can be reused, after initializing it again. - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} false if MD5 checking is on AND a STREAMINFO block was available AND the MD5 signature in - * the STREAMINFO block was non-zero AND the signature does not match the one computed by the decoder; - * else true. - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_finish: Module.cwrap('FLAC__stream_decoder_finish', 'number', [ 'number' ]), - /** - * Reset the decoder for reuse. - * - *

- * NOTE: Needs to be re-initialized, before it can be used again - * - * @param {number} decoder - * the ID of the decoder instance - * - * @returns {boolean} true if successful - * - * @see #init_decoder_stream - * @see #init_decoder_ogg_stream - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_reset: Module.cwrap('FLAC__stream_decoder_reset', 'number', [ 'number' ]), - /** - * Delete the encoder instance, and free up its resources. - * - * @param {number} encoder - * the ID of the encoder instance - * - * @memberOf Flac# - * @function - */ - FLAC__stream_encoder_delete: function(encoder){ - this._clear_enc_cb(encoder);//<- remove callback references - Module.ccall('FLAC__stream_encoder_delete', 'number', [ 'number' ], [encoder]); - do_fire_event('destroyed', [{type: 'destroyed', target: {id: encoder, type: 'encoder'}}], false); - }, - /** - * Delete the decoder instance, and free up its resources. - * - * @param {number} decoder - * the ID of the decoder instance - * - * @memberOf Flac# - * @function - */ - FLAC__stream_decoder_delete: function(decoder){ - this._clear_dec_cb(decoder);//<- remove callback references - Module.ccall('FLAC__stream_decoder_delete', 'number', [ 'number' ], [decoder]); - do_fire_event('destroyed', [{type: 'destroyed', target: {id: decoder, type: 'decoder'}}], false); - } - -};//END: var _exported = { - -//if Properties are supported by JS execution environment: -// support "immediate triggering" onready function, if library is already initialized when setting onready callback -if(typeof Object.defineProperty === 'function'){ - //add internal field for storing onready callback: - _exported._onready = void(0); - //define getter & define setter with "immediate trigger" functionality: - Object.defineProperty(_exported, 'onready', { - get() { return this._onready; }, - set(newValue) { - this._onready = newValue; - if(newValue && this.isReady()){ - check_and_trigger_persisted_event('ready', newValue); - } - } - }); -} else { - //if Properties are NOTE supported by JS execution environment: - // pring usage warning for onready hook instead - console.warn('WARN: note that setting Flac.onready handler after Flac.isReady() is already true, will have no effect, that is, the handler function will not be triggered!'); -} - -if(expLib && expLib.exports){ - expLib.exports = _exported; -} -return _exported; - -}));//END: UMD wrapper diff --git a/music_assistant/server/providers/snapcast/snapweb/config.js b/music_assistant/server/providers/snapcast/snapweb/config.js deleted file mode 100644 index 39bc3c0b..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/config.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; -let config = { - baseUrl: (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host -}; -//# sourceMappingURL=config.js.map diff --git a/music_assistant/server/providers/snapcast/snapweb/favicon.ico b/music_assistant/server/providers/snapcast/snapweb/favicon.ico deleted file mode 100644 index 1ec3fa87..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/favicon.ico and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/index.html b/music_assistant/server/providers/snapcast/snapweb/index.html deleted file mode 100644 index 1342e102..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - Snapweb - - - - - - - -

- - - diff --git a/music_assistant/server/providers/snapcast/snapweb/launcher-icon.png b/music_assistant/server/providers/snapcast/snapweb/launcher-icon.png deleted file mode 100644 index 8a005f12..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/launcher-icon.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/manifest.json b/music_assistant/server/providers/snapcast/snapweb/manifest.json deleted file mode 100644 index df145ff0..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "short_name": "Snapweb", - "name": "Snapcast WebApp", - "icons": [ - { - "src": "launcher-icon.png", - "sizes": "192x192", - "type": "image/png" - } - ], - "start_url": "/index.html", - "display": "standalone", - "categories": ["music"], - "description": "Snapcast web client", - "theme_color": "#455A64" -} diff --git a/music_assistant/server/providers/snapcast/snapweb/mute_icon.png b/music_assistant/server/providers/snapcast/snapweb/mute_icon.png deleted file mode 100644 index cf85867e..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/mute_icon.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/play.png b/music_assistant/server/providers/snapcast/snapweb/play.png deleted file mode 100644 index 41f76bbf..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/play.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/snapcast-512.png b/music_assistant/server/providers/snapcast/snapweb/snapcast-512.png deleted file mode 100644 index 96404404..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/snapcast-512.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/snapcontrol.js b/music_assistant/server/providers/snapcast/snapweb/snapcontrol.js deleted file mode 100644 index 0003b929..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/snapcontrol.js +++ /dev/null @@ -1,903 +0,0 @@ -"use strict"; -class Host { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.arch = json.arch; - this.ip = json.ip; - this.mac = json.mac; - this.name = json.name; - this.os = json.os; - } - arch = ""; - ip = ""; - mac = ""; - name = ""; - os = ""; -} -class Client { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.id = json.id; - this.host = new Host(json.host); - let jsnapclient = json.snapclient; - this.snapclient = { name: jsnapclient.name, protocolVersion: jsnapclient.protocolVersion, version: jsnapclient.version }; - let jconfig = json.config; - this.config = { instance: jconfig.instance, latency: jconfig.latency, name: jconfig.name, volume: { muted: jconfig.volume.muted, percent: jconfig.volume.percent } }; - this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec }; - this.connected = Boolean(json.connected); - } - id = ""; - host; - snapclient; - config; - lastSeen; - connected = false; -} -class Group { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.name = json.name; - this.id = json.id; - this.stream_id = json.stream_id; - this.muted = Boolean(json.muted); - for (let client of json.clients) - this.clients.push(new Client(client)); - } - name = ""; - id = ""; - stream_id = ""; - muted = false; - clients = []; - getClient(id) { - for (let client of this.clients) { - if (client.id == id) - return client; - } - return null; - } -} -class Metadata { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.title = json.title; - this.artist = json.artist; - this.album = json.album; - this.artUrl = json.artUrl; - this.duration = json.duration; - } - title; - artist; - album; - artUrl; - duration; -} -class Properties { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.loopStatus = json.loopStatus; - this.shuffle = json.shuffle; - this.volume = json.volume; - this.rate = json.rate; - this.playbackStatus = json.playbackStatus; - this.position = json.position; - this.minimumRate = json.minimumRate; - this.maximumRate = json.maximumRate; - this.canGoNext = Boolean(json.canGoNext); - this.canGoPrevious = Boolean(json.canGoPrevious); - this.canPlay = Boolean(json.canPlay); - this.canPause = Boolean(json.canPause); - this.canSeek = Boolean(json.canSeek); - this.canControl = Boolean(json.canControl); - if (json.metadata != undefined) { - this.metadata = new Metadata(json.metadata); - } - else { - this.metadata = new Metadata({}); - } - } - loopStatus; - shuffle; - volume; - rate; - playbackStatus; - position; - minimumRate; - maximumRate; - canGoNext = false; - canGoPrevious = false; - canPlay = false; - canPause = false; - canSeek = false; - canControl = false; - metadata; -} -class Stream { - constructor(json) { - this.fromJson(json); - } - fromJson(json) { - this.id = json.id; - this.status = json.status; - if (json.properties != undefined) { - this.properties = new Properties(json.properties); - } - else { - this.properties = new Properties({}); - } - let juri = json.uri; - this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query }; - } - id = ""; - status = ""; - uri; - properties; -} -class Server { - constructor(json) { - if (json) - this.fromJson(json); - } - fromJson(json) { - this.groups = []; - for (let jgroup of json.groups) - this.groups.push(new Group(jgroup)); - let jsnapserver = json.server.snapserver; - this.server = { host: new Host(json.server.host), snapserver: { controlProtocolVersion: jsnapserver.controlProtocolVersion, name: jsnapserver.name, protocolVersion: jsnapserver.protocolVersion, version: jsnapserver.version } }; - this.streams = []; - for (let jstream of json.streams) { - this.streams.push(new Stream(jstream)); - } - } - groups = []; - server; - streams = []; - getClient(id) { - for (let group of this.groups) { - let client = group.getClient(id); - if (client) - return client; - } - return null; - } - getGroup(id) { - for (let group of this.groups) { - if (group.id == id) - return group; - } - return null; - } - getStream(id) { - for (let stream of this.streams) { - if (stream.id == id) - return stream; - } - return null; - } -} -class SnapControl { - constructor(baseUrl) { - this.server = new Server(); - this.baseUrl = baseUrl; - this.msg_id = 0; - this.status_req_id = -1; - this.connect(); - } - connect() { - this.connection = new WebSocket(this.baseUrl + '/jsonrpc'); - this.connection.onmessage = (msg) => this.onMessage(msg.data); - this.connection.onopen = () => { this.status_req_id = this.sendRequest('Server.GetStatus'); }; - this.connection.onerror = (ev) => { console.error('error:', ev); }; - this.connection.onclose = () => { - console.info('connection lost, reconnecting in 1s'); - setTimeout(() => this.connect(), 1000); - }; - } - onNotification(notification) { - let stream; - switch (notification.method) { - case 'Client.OnVolumeChanged': - let client = this.getClient(notification.params.id); - client.config.volume = notification.params.volume; - updateGroupVolume(this.getGroupFromClient(client.id)); - return true; - case 'Client.OnLatencyChanged': - this.getClient(notification.params.id).config.latency = notification.params.latency; - return false; - case 'Client.OnNameChanged': - this.getClient(notification.params.id).config.name = notification.params.name; - return true; - case 'Client.OnConnect': - case 'Client.OnDisconnect': - this.getClient(notification.params.client.id).fromJson(notification.params.client); - return true; - case 'Group.OnMute': - this.getGroup(notification.params.id).muted = Boolean(notification.params.mute); - return true; - case 'Group.OnStreamChanged': - this.getGroup(notification.params.id).stream_id = notification.params.stream_id; - this.updateProperties(notification.params.stream_id); - return true; - case 'Stream.OnUpdate': - stream = this.getStream(notification.params.id); - stream.fromJson(notification.params.stream); - this.updateProperties(stream.id); - return true; - case 'Server.OnUpdate': - this.server.fromJson(notification.params.server); - this.updateProperties(this.getMyStreamId()); - return true; - case 'Stream.OnProperties': - stream = this.getStream(notification.params.id); - stream.properties.fromJson(notification.params.properties); - if (this.getMyStreamId() == stream.id) - this.updateProperties(stream.id); - return false; - default: - return false; - } - } - updateProperties(stream_id) { - if (!('mediaSession' in navigator)) { - console.log('updateProperties: mediaSession not supported'); - return; - } - if (stream_id != this.getMyStreamId()) { - console.log('updateProperties: not my stream id: ' + stream_id + ', mine: ' + this.getMyStreamId()); - return; - } - let props; - let metadata; - try { - props = this.getStreamFromClient(SnapStream.getClientId()).properties; - metadata = this.getStreamFromClient(SnapStream.getClientId()).properties.metadata; - } - catch (e) { - console.log('updateProperties failed: ' + e); - return; - } - // https://developers.google.com/web/updates/2017/02/media-session - // https://github.com/googlechrome/samples/tree/gh-pages/media-session - // https://googlechrome.github.io/samples/media-session/audio.html - // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession/setActionHandler#seekto - console.log('updateProperties: ', props); - let play_state = "none"; - if (props.playbackStatus != undefined) { - if (props.playbackStatus == "playing") { - audio.play(); - play_state = "playing"; - } - else if (props.playbackStatus == "paused") { - audio.pause(); - play_state = "paused"; - } - else if (props.playbackStatus == "stopped") { - audio.pause(); - play_state = "none"; - } - } - let mediaSession = navigator.mediaSession; - mediaSession.playbackState = play_state; - console.log('updateProperties playbackState: ', navigator.mediaSession.playbackState); - // if (props.canGoNext == undefined || !props.canGoNext!) - mediaSession.setActionHandler('play', () => { - props.canPlay ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'play' }) : null; - }); - mediaSession.setActionHandler('pause', () => { - props.canPause ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'pause' }) : null; - }); - mediaSession.setActionHandler('previoustrack', () => { - props.canGoPrevious ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'previous' }) : null; - }); - mediaSession.setActionHandler('nexttrack', () => { - props.canGoNext ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'next' }) : null; - }); - try { - mediaSession.setActionHandler('stop', () => { - props.canControl ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'stop' }) : null; - }); - } - catch (error) { - console.log('Warning! The "stop" media session action is not supported.'); - } - let defaultSkipTime = 10; // Time to skip in seconds by default - mediaSession.setActionHandler('seekbackward', (event) => { - let offset = (event.seekOffset || defaultSkipTime) * -1; - if (props.position != undefined) - Math.max(props.position + offset, 0); - props.canSeek ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null; - }); - mediaSession.setActionHandler('seekforward', (event) => { - let offset = event.seekOffset || defaultSkipTime; - if ((metadata.duration != undefined) && (props.position != undefined)) - Math.min(props.position + offset, metadata.duration); - props.canSeek ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null; - }); - try { - mediaSession.setActionHandler('seekto', (event) => { - let position = event.seekTime || 0; - if (metadata.duration != undefined) - Math.min(position, metadata.duration); - props.canSeek ? - this.sendRequest('Stream.Control', { id: stream_id, command: 'setPosition', params: { 'position': position } }) : null; - }); - } - catch (error) { - console.log('Warning! The "seekto" media session action is not supported.'); - } - if ((metadata.duration != undefined) && (props.position != undefined) && (props.position <= metadata.duration)) { - if ('setPositionState' in mediaSession) { - console.log('Updating position state: ' + props.position + '/' + metadata.duration); - mediaSession.setPositionState({ - duration: metadata.duration, - playbackRate: 1.0, - position: props.position - }); - } - } - else { - mediaSession.setPositionState({ - duration: 0, - playbackRate: 1.0, - position: 0 - }); - } - console.log('updateMetadata: ', metadata); - // https://github.com/Microsoft/TypeScript/issues/19473 - let title = metadata.title || "Unknown Title"; - let artist = (metadata.artist != undefined) ? metadata.artist.join(', ') : "Unknown Artist"; - let album = metadata.album || ""; - let artwork = [{ src: 'snapcast-512.png', sizes: '512x512', type: 'image/png' }]; - if (metadata.artUrl != undefined) { - artwork = [ - { src: metadata.artUrl, sizes: '96x96', type: 'image/png' }, - { src: metadata.artUrl, sizes: '128x128', type: 'image/png' }, - { src: metadata.artUrl, sizes: '192x192', type: 'image/png' }, - { src: metadata.artUrl, sizes: '256x256', type: 'image/png' }, - { src: metadata.artUrl, sizes: '384x384', type: 'image/png' }, - { src: metadata.artUrl, sizes: '512x512', type: 'image/png' }, - ]; - } // || 'snapcast-512.png'; - console.log('Metadata title: ' + title + ', artist: ' + artist + ', album: ' + album + ", artwork: " + artwork); - navigator.mediaSession.metadata = new MediaMetadata({ - title: title, - artist: artist, - album: album, - artwork: artwork - }); - // mediaSession.setActionHandler('seekbackward', function () { }); - // mediaSession.setActionHandler('seekforward', function () { }); - } - getClient(client_id) { - let client = this.server.getClient(client_id); - if (client == null) { - throw new Error(`client ${client_id} was null`); - } - return client; - } - getGroup(group_id) { - let group = this.server.getGroup(group_id); - if (group == null) { - throw new Error(`group ${group_id} was null`); - } - return group; - } - getGroupVolume(group, online) { - if (group.clients.length == 0) - return 0; - let group_vol = 0; - let client_count = 0; - for (let client of group.clients) { - if (online && !client.connected) - continue; - group_vol += client.config.volume.percent; - ++client_count; - } - if (client_count == 0) - return 0; - return group_vol / client_count; - } - getGroupFromClient(client_id) { - for (let group of this.server.groups) - for (let client of group.clients) - if (client.id == client_id) - return group; - throw new Error(`group for client ${client_id} was null`); - } - getStreamFromClient(client_id) { - let group = this.getGroupFromClient(client_id); - return this.getStream(group.stream_id); - } - getMyStreamId() { - try { - let group = this.getGroupFromClient(SnapStream.getClientId()); - return this.getStream(group.stream_id).id; - } - catch (e) { - return ""; - } - } - getStream(stream_id) { - let stream = this.server.getStream(stream_id); - if (stream == null) { - throw new Error(`stream ${stream_id} was null`); - } - return stream; - } - setVolume(client_id, percent, mute) { - percent = Math.max(0, Math.min(100, percent)); - let client = this.getClient(client_id); - client.config.volume.percent = percent; - if (mute != undefined) - client.config.volume.muted = mute; - this.sendRequest('Client.SetVolume', { id: client_id, volume: { muted: client.config.volume.muted, percent: client.config.volume.percent } }); - } - setClientName(client_id, name) { - let client = this.getClient(client_id); - let current_name = (client.config.name != "") ? client.config.name : client.host.name; - if (name != current_name) { - this.sendRequest('Client.SetName', { id: client_id, name: name }); - client.config.name = name; - } - } - setClientLatency(client_id, latency) { - let client = this.getClient(client_id); - let current_latency = client.config.latency; - if (latency != current_latency) { - this.sendRequest('Client.SetLatency', { id: client_id, latency: latency }); - client.config.latency = latency; - } - } - deleteClient(client_id) { - this.sendRequest('Server.DeleteClient', { id: client_id }); - this.server.groups.forEach((g, gi) => { - g.clients.forEach((c, ci) => { - if (c.id == client_id) { - this.server.groups[gi].clients.splice(ci, 1); - } - }); - }); - this.server.groups.forEach((g, gi) => { - if (g.clients.length == 0) { - this.server.groups.splice(gi, 1); - } - }); - show(); - } - setStream(group_id, stream_id) { - this.getGroup(group_id).stream_id = stream_id; - this.updateProperties(stream_id); - this.sendRequest('Group.SetStream', { id: group_id, stream_id: stream_id }); - } - setClients(group_id, clients) { - this.status_req_id = this.sendRequest('Group.SetClients', { id: group_id, clients: clients }); - } - muteGroup(group_id, mute) { - this.getGroup(group_id).muted = mute; - this.sendRequest('Group.SetMute', { id: group_id, mute: mute }); - } - sendRequest(method, params) { - let msg = { - id: ++this.msg_id, - jsonrpc: '2.0', - method: method - }; - if (params) - msg.params = params; - let msgJson = JSON.stringify(msg); - console.log("Sending: " + msgJson); - this.connection.send(msgJson); - return this.msg_id; - } - onMessage(msg) { - let json_msg = JSON.parse(msg); - let is_response = (json_msg.id != undefined); - console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(json_msg)); - if (is_response) { - if (json_msg.id == this.status_req_id) { - this.server = new Server(json_msg.result.server); - this.updateProperties(this.getMyStreamId()); - show(); - } - } - else { - let refresh = false; - if (Array.isArray(json_msg)) { - for (let notification of json_msg) { - refresh = this.onNotification(notification) || refresh; - } - } - else { - refresh = this.onNotification(json_msg); - } - // TODO: don't update everything, but only the changed, - // e.g. update the values for the volume sliders - if (refresh) - show(); - } - } - baseUrl; - connection; - server; - msg_id; - status_req_id; -} -let snapcontrol; -let snapstream = null; -let hide_offline = true; -let autoplay_done = false; -let audio = document.createElement('audio'); -function autoplayRequested() { - return document.location.hash.match(/autoplay/) !== null; -} -function show() { - // Render the page - const versionElem = document.getElementsByTagName("meta").namedItem("version"); - console.log("Snapweb version " + (versionElem ? versionElem.content : "null")); - let play_img; - if (snapstream) { - play_img = 'stop.png'; - } - else { - play_img = 'play.png'; - } - let content = ""; - content += ""; - content += "
"; - let server = snapcontrol.server; - for (let group of server.groups) { - if (hide_offline) { - let groupActive = false; - for (let client of group.clients) { - if (client.connected) { - groupActive = true; - break; - } - } - if (!groupActive) - continue; - } - // Set mute variables - let classgroup; - let muted; - let mute_img; - if (group.muted == true) { - classgroup = 'group muted'; - muted = true; - mute_img = 'mute_icon.png'; - } - else { - classgroup = 'group'; - muted = false; - mute_img = 'speaker_icon.png'; - } - // Start group div - content += "
"; - // Create stream selection dropdown - let streamselect = ""; - // Group mute and refresh button - content += "
"; - content += streamselect; - // let cover_img: string = server.getStream(group.stream_id)!.properties.metadata.artUrl || "snapcast-512.png"; - // content += ""; - let clientCount = 0; - for (let client of group.clients) - if (!hide_offline || client.connected) - clientCount++; - if (clientCount > 1) { - let volume = snapcontrol.getGroupVolume(group, hide_offline); - // content += "
"; - content += ""; - content += "
"; - content += " "; - // content += " "; - content += "
"; - // content += "
"; - } - // transparent placeholder edit icon - content += "
"; - content += "
"; - content += "
"; - // Create clients in group - for (let client of group.clients) { - if (!client.connected && hide_offline) - continue; - // Set name and connection state vars, start client div - let name; - let clas = 'client'; - if (client.config.name != "") { - name = client.config.name; - } - else { - name = client.host.name; - } - if (client.connected == false) { - clas = 'client disconnected'; - } - content += "
"; - // Client mute status vars - let muted; - let mute_img; - let sliderclass; - if (client.config.volume.muted == true) { - muted = true; - sliderclass = 'slider muted'; - mute_img = 'mute_icon.png'; - } - else { - sliderclass = 'slider'; - muted = false; - mute_img = 'speaker_icon.png'; - } - // Populate client div - content += ""; - content += "
"; - content += " "; - content += "
"; - content += " "; - content += " "; - if (client.connected == false) { - content += " 🗑"; - content += " "; - } - else { - content += ""; - } - content += "
" + name + "
"; - content += "
"; - } - content += "
"; - } - content += "
"; // content - content += "
"; - content += "
"; - content += "
"; - content += " "; - content += " "; - content += " "; - content += " "; - content += " "; - content += " "; - content += " "; - content += "
"; - content += "
"; - content += "
"; - // Pad then update page - content = content + "

"; - document.getElementById('show').innerHTML = content; - let playElem = document.getElementById('play-button'); - playElem.onclick = () => { - play(); - }; - for (let group of snapcontrol.server.groups) { - if (group.clients.length > 1) { - let slider = document.getElementById("vol_" + group.id); - if (slider == null) - continue; - slider.addEventListener('pointerdown', function () { - groupVolumeEnter(group.id); - }); - slider.addEventListener('touchstart', function () { - groupVolumeEnter(group.id); - }); - } - } -} -function updateGroupVolume(group) { - let group_vol = snapcontrol.getGroupVolume(group, hide_offline); - let slider = document.getElementById("vol_" + group.id); - if (slider == null) - return; - console.log("updateGroupVolume group: " + group.id + ", volume: " + group_vol + ", slider: " + (slider != null)); - slider.value = String(group_vol); -} -let client_volumes; -let group_volume; -function setGroupVolume(group_id) { - let group = snapcontrol.getGroup(group_id); - let percent = document.getElementById('vol_' + group.id).valueAsNumber; - console.log("setGroupVolume id: " + group.id + ", volume: " + percent); - // show() - let delta = percent - group_volume; - let ratio; - if (delta < 0) - ratio = (group_volume - percent) / group_volume; - else - ratio = (percent - group_volume) / (100 - group_volume); - for (let i = 0; i < group.clients.length; ++i) { - let new_volume = client_volumes[i]; - if (delta < 0) - new_volume -= ratio * client_volumes[i]; - else - new_volume += ratio * (100 - client_volumes[i]); - let client_id = group.clients[i].id; - // TODO: use batch request to update all client volumes at once - snapcontrol.setVolume(client_id, new_volume); - let slider = document.getElementById('vol_' + client_id); - if (slider) - slider.value = String(new_volume); - } -} -function groupVolumeEnter(group_id) { - let group = snapcontrol.getGroup(group_id); - let percent = document.getElementById('vol_' + group.id).valueAsNumber; - console.log("groupVolumeEnter id: " + group.id + ", volume: " + percent); - group_volume = percent; - client_volumes = []; - for (let i = 0; i < group.clients.length; ++i) { - client_volumes.push(group.clients[i].config.volume.percent); - } - // show() -} -function setVolume(id, mute) { - console.log("setVolume id: " + id + ", mute: " + mute); - let percent = document.getElementById('vol_' + id).valueAsNumber; - let client = snapcontrol.getClient(id); - let needs_update = (mute != client.config.volume.muted); - snapcontrol.setVolume(id, percent, mute); - let group = snapcontrol.getGroupFromClient(id); - updateGroupVolume(group); - if (needs_update) - show(); -} -function play() { - if (snapstream) { - snapstream.stop(); - snapstream = null; - audio.pause(); - audio.src = ''; - document.body.removeChild(audio); - } - else { - snapstream = new SnapStream(config.baseUrl); - // User interacted with the page. Let's play audio... - document.body.appendChild(audio); - audio.src = "10-seconds-of-silence.mp3"; - audio.loop = true; - audio.play().then(() => { - snapcontrol.updateProperties(snapcontrol.getMyStreamId()); - }); - } -} -function setMuteGroup(id, mute) { - snapcontrol.muteGroup(id, mute); - show(); -} -function setStream(id) { - snapcontrol.setStream(id, document.getElementById('stream_' + id).value); - show(); -} -function setGroup(client_id, group_id) { - console.log("setGroup id: " + client_id + ", group: " + group_id); - let server = snapcontrol.server; - // Get client group id - let current_group = snapcontrol.getGroupFromClient(client_id); - // Get - // List of target group's clients - // OR - // List of current group's other clients - let send_clients = []; - for (let i_group = 0; i_group < server.groups.length; i_group++) { - if (server.groups[i_group].id == group_id || (group_id == "new" && server.groups[i_group].id == current_group.id)) { - for (let i_client = 0; i_client < server.groups[i_group].clients.length; i_client++) { - if (group_id == "new" && server.groups[i_group].clients[i_client].id == client_id) { } - else { - send_clients[send_clients.length] = server.groups[i_group].clients[i_client].id; - } - } - } - } - if (group_id == "new") - group_id = current_group.id; - else - send_clients[send_clients.length] = client_id; - snapcontrol.setClients(group_id, send_clients); -} -function setName(id) { - // Get current name and lacency - let client = snapcontrol.getClient(id); - let current_name = (client.config.name != "") ? client.config.name : client.host.name; - let current_latency = client.config.latency; - let new_name = window.prompt("New Name", current_name); - let new_latency = Number(window.prompt("New Latency", String(current_latency))); - if (new_name != null) - snapcontrol.setClientName(id, new_name); - if (new_latency != null) - snapcontrol.setClientLatency(id, new_latency); - show(); -} -function openClientSettings(id) { - let modal = document.getElementById("client-settings"); - let client = snapcontrol.getClient(id); - let current_name = (client.config.name != "") ? client.config.name : client.host.name; - let name = document.getElementById("client-name"); - name.name = id; - name.value = current_name; - let latency = document.getElementById("client-latency"); - latency.valueAsNumber = client.config.latency; - let group = snapcontrol.getGroupFromClient(id); - let group_input = document.getElementById("client-group"); - while (group_input.length > 0) - group_input.remove(0); - let group_num = 0; - for (let ogroup of snapcontrol.server.groups) { - let option = document.createElement('option'); - option.value = ogroup.id; - option.text = "Group " + (group_num + 1) + " (" + ogroup.clients.length + " Clients)"; - group_input.add(option); - if (ogroup == group) { - console.log("Selected: " + group_num); - group_input.selectedIndex = group_num; - } - ++group_num; - } - let option = document.createElement('option'); - option.value = option.text = "new"; - group_input.add(option); - modal.style.display = "block"; -} -function closeClientSettings() { - let name = document.getElementById("client-name"); - let id = name.name; - console.log("onclose " + id + ", value: " + name.value); - snapcontrol.setClientName(id, name.value); - let latency = document.getElementById("client-latency"); - snapcontrol.setClientLatency(id, latency.valueAsNumber); - let group_input = document.getElementById("client-group"); - let option = group_input.options[group_input.selectedIndex]; - setGroup(id, option.value); - let modal = document.getElementById("client-settings"); - modal.style.display = "none"; - show(); -} -function deleteClient(id) { - if (confirm('Are you sure?')) { - snapcontrol.deleteClient(id); - } -} -window.onload = function () { - snapcontrol = new SnapControl(config.baseUrl); -}; -// When the user clicks anywhere outside of the modal, close it -window.onclick = function (event) { - let modal = document.getElementById("client-settings"); - if (event.target == modal) { - modal.style.display = "none"; - } -}; -//# sourceMappingURL=snapcontrol.js.map diff --git a/music_assistant/server/providers/snapcast/snapweb/snapstream.js b/music_assistant/server/providers/snapcast/snapweb/snapstream.js deleted file mode 100644 index b431c137..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/snapstream.js +++ /dev/null @@ -1,919 +0,0 @@ -"use strict"; -function setCookie(key, value, exdays = -1) { - let d = new Date(); - if (exdays < 0) - exdays = 10 * 365; - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); - let expires = "expires=" + d.toUTCString(); - document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/"; -} -function getPersistentValue(key, defaultValue = "") { - if (!!window.localStorage) { - const value = window.localStorage.getItem(key); - if (value !== null) { - return value; - } - window.localStorage.setItem(key, defaultValue); - return defaultValue; - } - // Fallback to cookies if localStorage is not available. - let name = key + "="; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for (let c of ca) { - c = c.trimLeft(); - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - setCookie(key, defaultValue); - return defaultValue; -} -function getChromeVersion() { - const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - return raw ? parseInt(raw[2]) : null; -} -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} -class Tv { - constructor(sec, usec) { - this.sec = sec; - this.usec = usec; - } - setMilliseconds(ms) { - this.sec = Math.floor(ms / 1000); - this.usec = Math.floor(ms * 1000) % 1000000; - } - getMilliseconds() { - return this.sec * 1000 + this.usec / 1000; - } - sec = 0; - usec = 0; -} -class BaseMessage { - constructor(_buffer) { - } - deserialize(buffer) { - let view = new DataView(buffer); - this.type = view.getUint16(0, true); - this.id = view.getUint16(2, true); - this.refersTo = view.getUint16(4, true); - this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true)); - this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true)); - this.size = view.getUint32(22, true); - } - serialize() { - this.size = 26 + this.getSize(); - let buffer = new ArrayBuffer(this.size); - let view = new DataView(buffer); - view.setUint16(0, this.type, true); - view.setUint16(2, this.id, true); - view.setUint16(4, this.refersTo, true); - view.setInt32(6, this.sent.sec, true); - view.setInt32(10, this.sent.usec, true); - view.setInt32(14, this.received.sec, true); - view.setInt32(18, this.received.usec, true); - view.setUint32(22, this.size, true); - return buffer; - } - getSize() { - return 0; - } - type = 0; - id = 0; - refersTo = 0; - received = new Tv(0, 0); - sent = new Tv(0, 0); - size = 0; -} -class CodecMessage extends BaseMessage { - constructor(buffer) { - super(buffer); - this.payload = new ArrayBuffer(0); - if (buffer) { - this.deserialize(buffer); - } - this.type = 1; - } - deserialize(buffer) { - super.deserialize(buffer); - let view = new DataView(buffer); - let codecSize = view.getInt32(26, true); - let decoder = new TextDecoder("utf-8"); - this.codec = decoder.decode(buffer.slice(30, 30 + codecSize)); - let payloadSize = view.getInt32(30 + codecSize, true); - console.log("payload size: " + payloadSize); - this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize); - console.log("payload: " + this.payload); - } - codec = ""; - payload; -} -class TimeMessage extends BaseMessage { - constructor(buffer) { - super(buffer); - if (buffer) { - this.deserialize(buffer); - } - this.type = 4; - } - deserialize(buffer) { - super.deserialize(buffer); - let view = new DataView(buffer); - this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true)); - } - serialize() { - let buffer = super.serialize(); - let view = new DataView(buffer); - view.setInt32(26, this.latency.sec, true); - view.setInt32(30, this.latency.usec, true); - return buffer; - } - getSize() { - return 8; - } - latency = new Tv(0, 0); -} -class JsonMessage extends BaseMessage { - constructor(buffer) { - super(buffer); - if (buffer) { - this.deserialize(buffer); - } - } - deserialize(buffer) { - super.deserialize(buffer); - let view = new DataView(buffer); - let size = view.getUint32(26, true); - let decoder = new TextDecoder(); - this.json = JSON.parse(decoder.decode(buffer.slice(30, 30 + size))); - } - serialize() { - let buffer = super.serialize(); - let view = new DataView(buffer); - let jsonStr = JSON.stringify(this.json); - view.setUint32(26, jsonStr.length, true); - let encoder = new TextEncoder(); - let encoded = encoder.encode(jsonStr); - for (let i = 0; i < encoded.length; ++i) - view.setUint8(30 + i, encoded[i]); - return buffer; - } - getSize() { - let encoder = new TextEncoder(); - let encoded = encoder.encode(JSON.stringify(this.json)); - return encoded.length + 4; - // return JSON.stringify(this.json).length; - } - json; -} -class HelloMessage extends JsonMessage { - constructor(buffer) { - super(buffer); - if (buffer) { - this.deserialize(buffer); - } - this.type = 5; - } - deserialize(buffer) { - super.deserialize(buffer); - this.mac = this.json["MAC"]; - this.hostname = this.json["HostName"]; - this.version = this.json["Version"]; - this.clientName = this.json["ClientName"]; - this.os = this.json["OS"]; - this.arch = this.json["Arch"]; - this.instance = this.json["Instance"]; - this.uniqueId = this.json["ID"]; - this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"]; - } - serialize() { - this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion }; - return super.serialize(); - } - mac = ""; - hostname = ""; - version = "0.0.0"; - clientName = "Snapweb"; - os = ""; - arch = "web"; - instance = 1; - uniqueId = ""; - snapStreamProtocolVersion = 2; -} -class ServerSettingsMessage extends JsonMessage { - constructor(buffer) { - super(buffer); - if (buffer) { - this.deserialize(buffer); - } - this.type = 3; - } - deserialize(buffer) { - super.deserialize(buffer); - this.bufferMs = this.json["bufferMs"]; - this.latency = this.json["latency"]; - this.volumePercent = this.json["volume"]; - this.muted = this.json["muted"]; - } - serialize() { - this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted }; - return super.serialize(); - } - bufferMs = 0; - latency = 0; - volumePercent = 0; - muted = false; -} -class PcmChunkMessage extends BaseMessage { - constructor(buffer, sampleFormat) { - super(buffer); - this.deserialize(buffer); - this.sampleFormat = sampleFormat; - this.type = 2; - } - deserialize(buffer) { - super.deserialize(buffer); - let view = new DataView(buffer); - this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true)); - // this.payloadSize = view.getUint32(34, true); - this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize); - // console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength); - } - readFrames(frames) { - let frameCnt = frames; - let frameSize = this.sampleFormat.frameSize(); - if (this.idx + frames > this.payloadSize() / frameSize) - frameCnt = (this.payloadSize() / frameSize) - this.idx; - let begin = this.idx * frameSize; - this.idx += frameCnt; - let end = begin + frameCnt * frameSize; - // console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength); - return this.payload.slice(begin, end); - } - getFrameCount() { - return (this.payloadSize() / this.sampleFormat.frameSize()); - } - isEndOfChunk() { - return this.idx >= this.getFrameCount(); - } - startMs() { - return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate); - } - duration() { - return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate); - } - payloadSize() { - return this.payload.byteLength; - } - clearPayload() { - this.payload = new ArrayBuffer(0); - } - addPayload(buffer) { - let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength); - let view = new DataView(payload); - let viewOld = new DataView(this.payload); - let viewNew = new DataView(buffer); - for (let i = 0; i < viewOld.byteLength; ++i) { - view.setInt8(i, viewOld.getInt8(i)); - } - for (let i = 0; i < viewNew.byteLength; ++i) { - view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i)); - } - this.payload = payload; - } - timestamp = new Tv(0, 0); - // payloadSize: number = 0; - payload = new ArrayBuffer(0); - idx = 0; - sampleFormat; -} -class AudioStream { - timeProvider; - sampleFormat; - bufferMs; - constructor(timeProvider, sampleFormat, bufferMs) { - this.timeProvider = timeProvider; - this.sampleFormat = sampleFormat; - this.bufferMs = bufferMs; - } - chunks = new Array(); - setVolume(percent, muted) { - // let base = 10; - this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1); - console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted); - this.muted = muted; - } - addChunk(chunk) { - this.chunks.push(chunk); - // let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds(); - // let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds(); - // console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2)); - while (this.chunks.length > 0) { - let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds(); - // todo: consider buffer ms - if (age > 5000 + this.bufferMs) { - this.chunks.shift(); - console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length); - } - else - break; - } - } - getNextBuffer(buffer, playTimeMs) { - if (!this.chunk) { - this.chunk = this.chunks.shift(); - } - // let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs; - let frames = buffer.length; - // console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2)); - let left = new Float32Array(frames); - let right = new Float32Array(frames); - let read = 0; - let pos = 0; - // let volume = this.muted ? 0 : this.volume; - let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs); - if (this.chunk) { - let age = serverPlayTimeMs - this.chunk.startMs(); // - 500; - let reqChunkDuration = frames / this.sampleFormat.msRate(); - let secs = Math.floor(Date.now() / 1000); - if (this.lastLog != secs) { - this.lastLog = secs; - console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration); - } - if (age < -reqChunkDuration) { - console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2)); - console.log("Chunk too young, returning silence"); - } - else { - if (Math.abs(age) > 5) { - // We are 5ms apart, do a hard sync, i.e. don't play faster/slower, - // but seek to the desired position instead - while (this.chunk && age > this.chunk.duration()) { - console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")"); - this.chunk = this.chunks.shift(); - if (!this.chunk) - break; - age = serverPlayTimeMs - this.chunk.startMs(); - } - if (this.chunk) { - if (age > 0) { - console.log("Fast forwarding " + age.toFixed(2) + "ms"); - this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate())); - } - else if (age < 0) { - console.log("Playing silence " + -age.toFixed(2) + "ms"); - let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate()); - left.fill(0, 0, silentFrames); - right.fill(0, 0, silentFrames); - read = silentFrames; - pos = silentFrames; - } - age = 0; - } - } - // else if (age > 0.1) { - // let rate = age * 0.0005; - // rate = 1.0 - Math.min(rate, 0.0005); - // console.debug("Age > 0, rate: " + rate); - // // we are late (age > 0), this means we are not playing fast enough - // // => the real sample rate seems to be lower, we have to drop some frames - // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999); - // } - // else if (age < -0.1) { - // let rate = -age * 0.0005; - // rate = 1.0 + Math.min(rate, 0.0005); - // console.debug("Age < 0, rate: " + rate); - // // we are early (age > 0), this means we are playing too fast - // // => the real sample rate seems to be higher, we have to insert some frames - // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999); - // } - // else { - // this.setRealSampleRate(this.sampleFormat.rate); - // } - let addFrames = 0; - let everyN = 0; - if (age > 0.1) { - addFrames = Math.ceil(age); // / 5); - } - else if (age < -0.1) { - addFrames = Math.floor(age); // / 5); - } - // addFrames = -2; - let readFrames = frames + addFrames - read; - if (addFrames != 0) - everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1)); - // addFrames = 0; - // console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN); - while ((read < readFrames) && this.chunk) { - let pcmChunk = this.chunk; - let pcmBuffer = pcmChunk.readFrames(readFrames - read); - let payload = new Int16Array(pcmBuffer); - // console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length); - // read += (pcmBuffer.byteLength / this.sampleFormat.frameSize()); - for (let i = 0; i < payload.length; i += 2) { - read++; - left[pos] = (payload[i] / 32768); // * volume; - right[pos] = (payload[i + 1] / 32768); // * volume; - if ((everyN != 0) && (read % everyN == 0)) { - if (addFrames > 0) { - pos--; - } - else { - left[pos + 1] = left[pos]; - right[pos + 1] = right[pos]; - pos++; - // console.log("Add: " + pos); - } - } - pos++; - } - if (pcmChunk.isEndOfChunk()) { - this.chunk = this.chunks.shift(); - } - } - if (addFrames != 0) - console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN); - if (read == readFrames) - read = frames; - } - } - if (read < frames) { - console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length); - left.fill(0, pos); - right.fill(0, pos); - } - // copyToChannel is not supported by Safari - buffer.getChannelData(0).set(left); - buffer.getChannelData(1).set(right); - } - // setRealSampleRate(sampleRate: number) { - // if (sampleRate == this.sampleFormat.rate) { - // this.correctAfterXFrames = 0; - // } - // else { - // this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.)); - // console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames); - // } - // } - chunk = undefined; - volume = 1; - muted = false; - lastLog = 0; -} -class TimeProvider { - constructor(ctx = undefined) { - if (ctx) { - this.setAudioContext(ctx); - } - } - setAudioContext(ctx) { - this.ctx = ctx; - this.reset(); - } - reset() { - this.diffBuffer.length = 0; - this.diff = 0; - } - setDiff(c2s, s2c) { - if (this.now() == 0) { - this.reset(); - } - else { - if (this.diffBuffer.push((c2s - s2c) / 2) > 100) - this.diffBuffer.shift(); - let sorted = [...this.diffBuffer]; - sorted.sort(); - this.diff = sorted[Math.floor(sorted.length / 2)]; - } - // console.debug("c2s: " + c2s.toFixed(2) + ", s2c: " + s2c.toFixed(2) + ", diff: " + this.diff.toFixed(2) + ", now: " + this.now().toFixed(2) + ", server.now: " + this.serverNow().toFixed(2) + ", win.now: " + window.performance.now().toFixed(2)); - // console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now()); - } - now() { - if (!this.ctx) { - return window.performance.now(); - } - else { - // Use the more accurate getOutputTimestamp if available, fallback to ctx.currentTime otherwise. - const contextTime = !!this.ctx.getOutputTimestamp ? this.ctx.getOutputTimestamp().contextTime : undefined; - return (contextTime !== undefined ? contextTime : this.ctx.currentTime) * 1000; - } - } - nowSec() { - return this.now() / 1000; - } - serverNow() { - return this.serverTime(this.now()); - } - serverTime(localTimeMs) { - return localTimeMs + this.diff; - } - diffBuffer = new Array(); - diff = 0; - ctx; -} -class SampleFormat { - rate = 48000; - channels = 2; - bits = 16; - msRate() { - return this.rate / 1000; - } - toString() { - return this.rate + ":" + this.bits + ":" + this.channels; - } - sampleSize() { - if (this.bits == 24) { - return 4; - } - return this.bits / 8; - } - frameSize() { - return this.channels * this.sampleSize(); - } - durationMs(bytes) { - return (bytes / this.frameSize()) * this.msRate(); - } -} -class Decoder { - setHeader(_buffer) { - return new SampleFormat(); - } - decode(_chunk) { - return null; - } -} -class OpusDecoder extends Decoder { - setHeader(buffer) { - let view = new DataView(buffer); - let ID_OPUS = 0x4F505553; - if (buffer.byteLength < 12) { - console.error("Opus header too small: " + buffer.byteLength); - return null; - } - else if (view.getUint32(0, true) != ID_OPUS) { - console.error("Opus header too small: " + buffer.byteLength); - return null; - } - let format = new SampleFormat(); - format.rate = view.getUint32(4, true); - format.bits = view.getUint16(8, true); - format.channels = view.getUint16(10, true); - console.log("Opus samplerate: " + format.toString()); - return format; - } - decode(_chunk) { - return null; - } -} -class FlacDecoder extends Decoder { - constructor() { - super(); - this.decoder = Flac.create_libflac_decoder(true); - if (this.decoder) { - let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false); - console.log("Flac init: " + init_status); - Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true }); - } - this.sampleFormat = new SampleFormat(); - this.flacChunk = new ArrayBuffer(0); - // this.pcmChunk = new PcmChunkMessage(); - // Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals}); - // flac_ok &= init_status == 0; - // console.log("flac init : " + flac_ok);//DEBUG - } - decode(chunk) { - // console.log("Flac decode: " + chunk.payload.byteLength); - this.flacChunk = chunk.payload.slice(0); - this.pcmChunk = chunk; - this.pcmChunk.clearPayload(); - this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true }; - // console.log("Flac len: " + this.flacChunk.byteLength); - while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) { - Flac.FLAC__stream_decoder_get_state(this.decoder); - // let state = Flac.FLAC__stream_decoder_get_state(this.decoder); - // console.log("State: " + state); - } - // console.log("Pcm payload: " + this.pcmChunk!.payloadSize()); - if (this.cacheInfo.cachedBlocks > 0) { - let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate(); - // console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms"); - this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs); - } - return this.pcmChunk; - } - read_callback_fn(bufferSize) { - // console.log(' decode read callback, buffer bytes max=', bufferSize); - if (this.header) { - console.log(" header: " + this.header.byteLength); - let data = new Uint8Array(this.header); - this.header = null; - return { buffer: data, readDataLength: data.byteLength, error: false }; - } - else if (this.flacChunk) { - // console.log(" flacChunk: " + this.flacChunk.byteLength); - // a fresh read => next call to write will not be from cached data - this.cacheInfo.isCachedChunk = false; - let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength))); - this.flacChunk = this.flacChunk.slice(data.byteLength); - return { buffer: data, readDataLength: data.byteLength, error: false }; - } - return { buffer: new Uint8Array(0), readDataLength: 0, error: false }; - } - write_callback_fn(data, frameInfo) { - // console.log(" write frame metadata: " + frameInfo + ", len: " + data.length); - if (this.cacheInfo.isCachedChunk) { - // there was no call to read, so it's some cached data - this.cacheInfo.cachedBlocks += frameInfo.blocksize; - } - let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize); - let view = new DataView(payload); - for (let channel = 0; channel < frameInfo.channels; ++channel) { - let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength); - // console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize); - for (let i = 0; i < frameInfo.blocksize; ++i) { - view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true); - } - } - this.pcmChunk.addPayload(payload); - // console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize()); - } - /** @memberOf decode */ - metadata_callback_fn(data) { - console.info('meta data: ', data); - // let view = new DataView(data); - this.sampleFormat.rate = data.sampleRate; - this.sampleFormat.channels = data.channels; - this.sampleFormat.bits = data.bitsPerSample; - console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString()); - } - /** @memberOf decode */ - error_callback_fn(err, errMsg) { - console.error('decode error callback', err, errMsg); - } - setHeader(buffer) { - this.header = buffer.slice(0); - Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder); - return this.sampleFormat; - } - sampleFormat; - decoder; - header = null; - flacChunk; - pcmChunk; - cacheInfo = { isCachedChunk: false, cachedBlocks: 0 }; -} -class PlayBuffer { - constructor(buffer, playTime, source, destination) { - this.buffer = buffer; - this.playTime = playTime; - this.source = source; - this.source.buffer = this.buffer; - this.source.connect(destination); - this.onended = (_playBuffer) => { }; - } - onended; - start() { - this.source.onended = () => { - this.onended(this); - }; - this.source.start(this.playTime); - } - buffer; - playTime; - source; - num = 0; -} -class PcmDecoder extends Decoder { - setHeader(buffer) { - let sampleFormat = new SampleFormat(); - let view = new DataView(buffer); - sampleFormat.channels = view.getUint16(22, true); - sampleFormat.rate = view.getUint32(24, true); - sampleFormat.bits = view.getUint16(34, true); - return sampleFormat; - } - decode(chunk) { - return chunk; - } -} -class SnapStream { - constructor(baseUrl) { - this.baseUrl = baseUrl; - this.timeProvider = new TimeProvider(); - if (this.setupAudioContext()) { - this.connect(); - } - else { - alert("Sorry, but the Web Audio API is not supported by your browser"); - } - } - setupAudioContext() { - let AudioContext = window.AudioContext // Default - || window.webkitAudioContext // Safari and old versions of Chrome - || false; - if (AudioContext) { - let options; - options = { latencyHint: "playback", sampleRate: this.sampleFormat ? this.sampleFormat.rate : undefined }; - const chromeVersion = getChromeVersion(); - if ((chromeVersion !== null && chromeVersion < 55) || !window.AudioContext) { - // Some older browsers won't decode the stream if options are provided. - options = undefined; - } - this.ctx = new AudioContext(options); - this.gainNode = this.ctx.createGain(); - this.gainNode.connect(this.ctx.destination); - } - else { - // Web Audio API is not supported - return false; - } - return true; - } - static getClientId() { - return getPersistentValue("uniqueId", uuidv4()); - } - connect() { - this.streamsocket = new WebSocket(this.baseUrl + '/stream'); - this.streamsocket.binaryType = "arraybuffer"; - this.streamsocket.onmessage = (ev) => this.onMessage(ev); - this.streamsocket.onopen = () => { - console.log("on open"); - let hello = new HelloMessage(); - hello.mac = "00:00:00:00:00:00"; - hello.arch = "web"; - hello.os = navigator.platform; - hello.hostname = "Snapweb client"; - hello.uniqueId = SnapStream.getClientId(); - const versionElem = document.getElementsByTagName("meta").namedItem("version"); - hello.version = versionElem ? versionElem.content : "0.0.0"; - this.sendMessage(hello); - this.syncTime(); - this.syncHandle = window.setInterval(() => this.syncTime(), 1000); - }; - this.streamsocket.onerror = (ev) => { console.error('error:', ev); }; - this.streamsocket.onclose = () => { - window.clearInterval(this.syncHandle); - console.info('connection lost, reconnecting in 1s'); - setTimeout(() => this.connect(), 1000); - }; - } - onMessage(msg) { - let view = new DataView(msg.data); - let type = view.getUint16(0, true); - if (type == 1) { - let codec = new CodecMessage(msg.data); - console.log("Codec: " + codec.codec); - if (codec.codec == "flac") { - this.decoder = new FlacDecoder(); - } - else if (codec.codec == "pcm") { - this.decoder = new PcmDecoder(); - } - else if (codec.codec == "opus") { - this.decoder = new OpusDecoder(); - alert("Codec not supported: " + codec.codec); - } - else { - alert("Codec not supported: " + codec.codec); - } - if (this.decoder) { - this.sampleFormat = this.decoder.setHeader(codec.payload); - console.log("Sampleformat: " + this.sampleFormat.toString()); - if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) { - alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString()); - } - else { - if (this.bufferDurationMs != 0) { - this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate()); - } - if (window.AudioContext) { - // we are not using webkitAudioContext, so it's safe to setup a new AudioContext with the new samplerate - // since this code is not triggered by direct user input, we cannt create a webkitAudioContext here - this.stopAudio(); - this.setupAudioContext(); - } - this.ctx.resume(); - this.timeProvider.setAudioContext(this.ctx); - this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100; - // this.timeProvider = new TimeProvider(this.ctx); - this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs); - this.latency = (this.ctx.baseLatency !== undefined ? this.ctx.baseLatency : 0) + (this.ctx.outputLatency !== undefined ? this.ctx.outputLatency : 0); - console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency + ", latency: " + this.latency); - this.play(); - } - } - } - else if (type == 2) { - let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat); - if (this.decoder) { - let decoded = this.decoder.decode(pcmChunk); - if (decoded) { - this.stream.addChunk(decoded); - } - } - } - else if (type == 3) { - this.serverSettings = new ServerSettingsMessage(msg.data); - this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100; - this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency; - console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted); - } - else if (type == 4) { - if (this.timeProvider) { - let time = new TimeMessage(msg.data); - this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds()); - } - // console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff); - } - else { - console.info("Message not handled, type: " + type); - } - } - sendMessage(msg) { - msg.sent = new Tv(0, 0); - msg.sent.setMilliseconds(this.timeProvider.now()); - msg.id = ++this.msgId; - if (this.streamsocket.readyState == this.streamsocket.OPEN) { - this.streamsocket.send(msg.serialize()); - } - } - syncTime() { - let t = new TimeMessage(); - t.latency.setMilliseconds(this.timeProvider.now()); - this.sendMessage(t); - // console.log("prepareSource median: " + Math.round(this.median * 10) / 10); - } - stopAudio() { - // if (this.ctx) { - // this.ctx.close(); - // } - this.ctx.suspend(); - while (this.audioBuffers.length > 0) { - let buffer = this.audioBuffers.pop(); - buffer.onended = () => { }; - buffer.source.stop(); - } - while (this.freeBuffers.length > 0) { - this.freeBuffers.pop(); - } - } - stop() { - window.clearInterval(this.syncHandle); - this.stopAudio(); - if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) { - this.streamsocket.onclose = () => { }; - this.streamsocket.close(); - } - } - play() { - this.playTime = this.timeProvider.nowSec() + 0.1; - for (let i = 1; i <= this.audioBufferCount; ++i) { - this.playNext(); - } - } - playNext() { - let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate); - let playTimeMs = (this.playTime + this.latency) * 1000 - this.bufferMs; - this.stream.getNextBuffer(buffer, playTimeMs); - let source = this.ctx.createBufferSource(); - let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode); - this.audioBuffers.push(playBuffer); - playBuffer.num = ++this.bufferNum; - playBuffer.onended = (buffer) => { - // let diff = this.timeProvider.nowSec() - buffer.playTime; - this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer); - // console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length); - this.playNext(); - }; - playBuffer.start(); - this.playTime += this.bufferFrameCount / this.sampleFormat.rate; - } - baseUrl; - streamsocket; - playTime = 0; - msgId = 0; - bufferDurationMs = 80; // 0; - bufferFrameCount = 3844; // 9600; // 2400;//8192; - syncHandle = -1; - // ageBuffer: Array; - audioBuffers = new Array(); - freeBuffers = new Array(); - timeProvider; - stream; - ctx; // | undefined; - gainNode; - serverSettings; - decoder; - sampleFormat; - // median: number = 0; - audioBufferCount = 3; - bufferMs = 1000; - bufferNum = 0; - latency = 0; -} -//# sourceMappingURL=snapstream.js.map diff --git a/music_assistant/server/providers/snapcast/snapweb/speaker_icon.png b/music_assistant/server/providers/snapcast/snapweb/speaker_icon.png deleted file mode 100644 index ae8554d7..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/speaker_icon.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/stop.png b/music_assistant/server/providers/snapcast/snapweb/stop.png deleted file mode 100644 index 0d55b491..00000000 Binary files a/music_assistant/server/providers/snapcast/snapweb/stop.png and /dev/null differ diff --git a/music_assistant/server/providers/snapcast/snapweb/styles.css b/music_assistant/server/providers/snapcast/snapweb/styles.css deleted file mode 100644 index 06a8092a..00000000 --- a/music_assistant/server/providers/snapcast/snapweb/styles.css +++ /dev/null @@ -1,314 +0,0 @@ -body { - background-color: rgb(246, 246, 246); - color: rgb(255, 255, 255); - font-family: 'Arial', sans-serif; - width: 100%; - margin: 0; - font-size: 20px; - overscroll-behavior: contain; -} - -/* width */ -::-webkit-scrollbar { - width: 10px; -} - -/* Track */ -::-webkit-scrollbar-track { - background: #1f1f1f; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: #333; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: #555; -} - -input, textarea, button, select, a { - -webkit-tap-highlight-color: rgba(0,0,0,0); -} - -.navbar { - overflow: hidden; - background-color: #607d8b; - z-index: 1; /* Sit on top */ - padding: 13px; - color: white; - position: fixed; /* Set the navbar to fixed position */ - top: 0; /* Position the navbar at the top of the page */ - width: 100%; /* Full width */ - font-size: 21px; - font-weight: 500; - user-select: none; -} - -.play-button { - display: block; - position: absolute; - right: 34px; - top: 5px; - height: 40px; - width: 40px; -} - -.content { - margin-top: 62px -} - -.group { - float: none; - background-color: white; - box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.2); - clear: both; - padding: 8px; - margin: 10px 15px 10px 15px; - overflow: auto; - width: auto; - border-radius: 3px; - user-select: none; -} - -.group.muted { - opacity: 0.27; -} - -.groupheader { - /* margin: 10px; */ - width: auto; - height: fit-content; - /* padding: 10px; */ - padding-bottom: 0px; - display: grid; - grid-template-columns: min-content auto min-content; - grid-template-rows: min-content min-content; - grid-gap: 0px; -} - -.groupheader-separator { - height: 1px; - margin: 8px 0px; - border-width: 0px; - color: lightgray; - background-color: lightgray; -} - -.stream { - color: #686868; - grid-row: 1; - grid-column: 1/3; - width: fit-content; -} - -select { - background-color: transparent; - border: 0px; - width: 150px; - font-size: 20px; - color: #e3e3e3; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; -} - -.slidergroupdiv { - /* background: greenyellow; */ - display: flex; - justify-content: center; - align-items: center; - grid-row: 2; - grid-column: 2; -} - -.client { - /* text-align: left; */ - /* margin: 10px; */ - width: auto; - height: fit-content; - /* padding: 10px; */ - display: grid; - grid-template-columns: min-content auto min-content; - grid-template-rows: min-content min-content; - grid-gap: 0px; -/* align-items: center;*/ -} - -/* .client:hover { - box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); -} */ - -.client.disconnected { - opacity: 0.27; -} - -.name { - color: #686868; - user-select: none; - /* background: red; */ - padding-top: 5px; - grid-row: 1; - grid-column: 1/3; - text-decoration: none; -} - -.editdiv { - background: violet; - grid-row: 0/4; - grid-column: 3; -} - -.edit-icon { - color: #686868; - text-decoration: none; -} - -.delete-icon { - color: #ff4081; - text-decoration: none; -} - -.edit-icons { - align-items: center; - display: flex; - grid-row: 1/3; - grid-column: 3; -} - -.edit-group-icon { - display: flex; - color: transparent; - align-items: center; - grid-row: 1/3; - grid-column: 3; - text-decoration: none; -} - -.mute-button { - color: #686868; - grid-row: 2; - grid-column: 1; -/* top: 50%;*/ - height: 25px; - width: 25px; - padding-left: 10px; - padding-right: 10px; - text-decoration: none; -} -/* -.cover-img { - color: #686868; - grid-row: 2; - grid-column: 1; - height: 50px; - width: 50px; - padding: 5px; - text-decoration: none; -} -*/ -.sliderdiv { - display: flex; - justify-content: center; - align-items: center; - grid-row: 2; - grid-column: 2; - /* padding-left: 40px; */ - /* display: inline-block; - text-align: left; - width: 250px; */ -} - -.slider { - writing-mode: bt-lr; - -webkit-appearance: none; - background: #dbdbdb; - outline: none; - -webkit-transition: .2s; - transition: opacity .2s; - height: 2px; - width: 90%; -} - -.slider::-moz-range-track { - padding: 6px; - background-color: transparent; - border: none; -} - -.slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - height: 12px; - width: 12px; - border-radius: 50%; - background: #ff4081; - cursor: pointer; -} - -.slider::-moz-range-thumb { - height: 12px; - width: 12px; - border-radius: 50%; - background: #ff4081; - cursor: pointer; -} - -.slider.muted { - opacity: 0.27; -} - - .client-settings { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - overflow: auto; /* Enable scroll if needed */ - background-color: rgb(0,0,0); /* Fallback color */ - background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ -} - -.client-setting-content { - background-color: #fefefe; - color: #686868; - margin: 15% auto; /* 15% from the top and centered */ - padding: 20px; - border: 1px solid #888; - width: 80%; /* Could be more or less, depending on screen size */ -} - -.client-input { - color: #686868; - width: 100%; - padding: 12px 20px; - margin: 8px 0; - display: block; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; -} - -input[type=submit] { - width: 100%; - background-color: #4CAF50; - color: white; - padding: 14px 20px; - margin: 8px 0; - border: none; - border-radius: 4px; - cursor: pointer; -} - -input[type=submit]:hover { - background-color: #45a049; -} - -div.container { - border-radius: 5px; - background-color: #f2f2f2; - padding: 20px; -} diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py deleted file mode 100644 index bdc900d9..00000000 --- a/music_assistant/server/providers/sonos/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Sonos Player provider for Music Assistant for speakers running the S2 firmware. - -Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. -https://github.com/music-assistant/aiosonos -""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.constants import VERBOSE_LOG_LEVEL - -from .provider import SonosPlayerProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - prov = SonosPlayerProvider(mass, manifest, config) - # set-up aiosonos logging - if prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("aiosonos").setLevel(logging.DEBUG) - else: - logging.getLogger("aiosonos").setLevel(prov.logger.level + 10) - return prov - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return () diff --git a/music_assistant/server/providers/sonos/const.py b/music_assistant/server/providers/sonos/const.py deleted file mode 100644 index 75ff2933..00000000 --- a/music_assistant/server/providers/sonos/const.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Constants for the Sonos (S2) provider.""" - -from __future__ import annotations - -from aiosonos.api.models import PlayBackState as SonosPlayBackState - -from music_assistant.common.models.enums import PlayerFeature, PlayerState - -PLAYBACK_STATE_MAP = { - SonosPlayBackState.PLAYBACK_STATE_BUFFERING: PlayerState.PLAYING, - SonosPlayBackState.PLAYBACK_STATE_IDLE: PlayerState.IDLE, - SonosPlayBackState.PLAYBACK_STATE_PAUSED: PlayerState.PAUSED, - SonosPlayBackState.PLAYBACK_STATE_PLAYING: PlayerState.PLAYING, -} - -PLAYER_FEATURES_BASE = { - PlayerFeature.SYNC, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.PAUSE, - PlayerFeature.ENQUEUE, -} - -SOURCE_LINE_IN = "line_in" -SOURCE_AIRPLAY = "airplay" -SOURCE_SPOTIFY = "spotify" -SOURCE_UNKNOWN = "unknown" -SOURCE_RADIO = "radio" - -CONF_AIRPLAY_MODE = "airplay_mode" diff --git a/music_assistant/server/providers/sonos/helpers.py b/music_assistant/server/providers/sonos/helpers.py deleted file mode 100644 index 3a6c6f02..00000000 --- a/music_assistant/server/providers/sonos/helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Helpers for the Sonos (S2) Provider.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from zeroconf import IPVersion - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - -def get_primary_ip_address(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - for address in discovery_info.parsed_addresses(IPVersion.V4Only): - if address.startswith("127"): - # filter out loopback address - continue - if address.startswith("169.254"): - # filter out APIPA address - continue - return address - return None diff --git a/music_assistant/server/providers/sonos/icon.svg b/music_assistant/server/providers/sonos/icon.svg deleted file mode 100644 index 60d9e677..00000000 --- a/music_assistant/server/providers/sonos/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/sonos/manifest.json b/music_assistant/server/providers/sonos/manifest.json deleted file mode 100644 index ba904cd3..00000000 --- a/music_assistant/server/providers/sonos/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "player", - "domain": "sonos", - "name": "SONOS", - "description": "SONOS Player provider for Music Assistant.", - "codeowners": ["@music-assistant"], - "requirements": ["aiosonos==0.1.6"], - "documentation": "https://music-assistant.io/player-support/sonos/", - "multi_instance": false, - "builtin": false, - "mdns_discovery": ["_sonos._tcp.local."] -} diff --git a/music_assistant/server/providers/sonos/player.py b/music_assistant/server/providers/sonos/player.py deleted file mode 100644 index 45bb672d..00000000 --- a/music_assistant/server/providers/sonos/player.py +++ /dev/null @@ -1,471 +0,0 @@ -""" -Sonos Player provider for Music Assistant for speakers running the S2 firmware. - -Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. -https://github.com/music-assistant/aiosonos - -SonosPlayer: Holds the details of the (discovered) Sonosplayer. -""" - -from __future__ import annotations - -import asyncio -import time -from collections.abc import Callable -from typing import TYPE_CHECKING - -import shortuuid -from aiohttp.client_exceptions import ClientConnectorError -from aiosonos.api.models import ContainerType, MusicService, SonosCapability -from aiosonos.api.models import PlayBackState as SonosPlayBackState -from aiosonos.client import SonosLocalApiClient -from aiosonos.const import EventType as SonosEventType -from aiosonos.const import SonosEvent -from aiosonos.exceptions import ConnectionFailed, FailedCommand - -from music_assistant.common.models.enums import ( - EventType, - PlayerFeature, - PlayerState, - PlayerType, - RepeatMode, -) -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import CONF_CROSSFADE - -from .const import ( - CONF_AIRPLAY_MODE, - PLAYBACK_STATE_MAP, - PLAYER_FEATURES_BASE, - SOURCE_AIRPLAY, - SOURCE_LINE_IN, - SOURCE_RADIO, - SOURCE_SPOTIFY, -) - -if TYPE_CHECKING: - from aiosonos.api.models import DiscoveryInfo as SonosDiscoveryInfo - - from .provider import SonosPlayerProvider - - -class SonosPlayer: - """Holds the details of the (discovered) Sonosplayer.""" - - def __init__( - self, - prov: SonosPlayerProvider, - player_id: str, - discovery_info: SonosDiscoveryInfo, - ip_address: str, - ) -> None: - """Initialize the SonosPlayer.""" - self.prov = prov - self.mass = prov.mass - self.player_id = player_id - self.discovery_info = discovery_info - self.ip_address = ip_address - self.logger = prov.logger.getChild(player_id) - self.connected: bool = False - self.client = SonosLocalApiClient(self.ip_address, self.mass.http_session) - self.mass_player: Player | None = None - self._listen_task: asyncio.Task | None = None - # Sonos speakers can optionally have airplay (most S2 speakers do) - # and this airplay player can also be a player within MA. - # We can do some smart stuff if we link them together where possible. - # The player we can just guess from the sonos player id (mac address). - self.airplay_player_id = f"ap{self.player_id[7:-5].lower()}" - self.queue_version: str = shortuuid.random(8) - self._on_cleanup_callbacks: list[Callable[[], None]] = [] - - def get_linked_airplay_player( - self, enabled_only: bool = True, active_only: bool = False - ) -> Player | None: - """Return the linked airplay player if available/enabled.""" - if enabled_only and not self.mass.config.get_raw_player_config_value( - self.player_id, CONF_AIRPLAY_MODE - ): - return None - if not (airplay_player := self.mass.players.get(self.airplay_player_id)): - return None - if not airplay_player.available: - return None - if active_only and not airplay_player.powered: - return None - return airplay_player - - async def setup(self) -> None: - """Handle setup of the player.""" - # connect the player first so we can fail early - await self._connect(False) - - # collect supported features - supported_features = set(PLAYER_FEATURES_BASE) - if SonosCapability.AUDIO_CLIP in self.discovery_info["device"]["capabilities"]: - supported_features.add(PlayerFeature.PLAY_ANNOUNCEMENT) - if not self.client.player.has_fixed_volume: - supported_features.add(PlayerFeature.VOLUME_SET) - - # instantiate the MA player - self.mass_player = mass_player = Player( - player_id=self.player_id, - provider=self.prov.instance_id, - type=PlayerType.PLAYER, - name=self.discovery_info["device"]["name"] - or self.discovery_info["device"]["modelDisplayName"], - available=True, - # treat as powered at start if the player is playing/paused - powered=self.client.player.group.playback_state - in ( - SonosPlayBackState.PLAYBACK_STATE_PLAYING, - SonosPlayBackState.PLAYBACK_STATE_BUFFERING, - SonosPlayBackState.PLAYBACK_STATE_PAUSED, - ), - device_info=DeviceInfo( - model=self.discovery_info["device"]["modelDisplayName"], - manufacturer=self.prov.manifest.name, - address=self.ip_address, - ), - supported_features=tuple(supported_features), - ) - self.update_attributes() - await self.mass.players.register_or_update(mass_player) - - # register callback for state changed - self._on_cleanup_callbacks.append( - self.client.subscribe( - self._on_player_event, - ( - SonosEventType.GROUP_UPDATED, - SonosEventType.PLAYER_UPDATED, - ), - ) - ) - # register callback for airplay player state changes - self._on_cleanup_callbacks.append( - self.mass.subscribe( - self._on_airplay_player_event, - (EventType.PLAYER_UPDATED, EventType.PLAYER_ADDED), - self.airplay_player_id, - ) - ) - # register callback for playerqueue state changes - self._on_cleanup_callbacks.append( - self.mass.subscribe( - self._on_mass_queue_items_event, - EventType.QUEUE_ITEMS_UPDATED, - self.player_id, - ) - ) - - async def unload(self) -> None: - """Unload the player (disconnect + cleanup).""" - await self._disconnect() - self.mass.players.remove(self.player_id, False) - for callback in self._on_cleanup_callbacks: - callback() - - def reconnect(self, delay: float = 1) -> None: - """Reconnect the player.""" - # use a task_id to prevent multiple reconnects - task_id = f"sonos_reconnect_{self.player_id}" - self.mass.call_later(delay, self._connect, delay, task_id=task_id) - - async def cmd_stop(self) -> None: - """Send STOP command to given player.""" - if self.client.player.is_passive: - self.logger.debug("Ignore STOP command: Player is synced to another player.") - return - if ( - airplay := self.get_linked_airplay_player(True, True) - ) and airplay.state != PlayerState.IDLE: - # linked airplay player is active, redirect the command - self.logger.debug("Redirecting STOP command to linked airplay player.") - await self.mass.players.cmd_stop(airplay.player_id) - return - try: - await self.client.player.group.stop() - except FailedCommand as err: - if "ERROR_PLAYBACK_NO_CONTENT" not in str(err): - raise - - async def cmd_play(self) -> None: - """Send PLAY command to given player.""" - if self.client.player.is_passive: - self.logger.debug("Ignore STOP command: Player is synced to another player.") - return - if ( - airplay := self.get_linked_airplay_player(True, True) - ) and airplay.state != PlayerState.IDLE: - # linked airplay player is active, redirect the command - self.logger.debug("Redirecting PLAY command to linked airplay player.") - await self.mass.players.cmd_play(airplay.player_id) - return - await self.client.player.group.play() - - async def cmd_pause(self) -> None: - """Send PAUSE command to given player.""" - if self.client.player.is_passive: - self.logger.debug("Ignore STOP command: Player is synced to another player.") - return - if ( - airplay := self.get_linked_airplay_player(True, True) - ) and airplay.state != PlayerState.IDLE: - # linked airplay player is active, redirect the command - self.logger.debug("Redirecting PAUSE command to linked airplay player.") - await self.mass.players.cmd_pause(airplay.player_id) - return - await self.client.player.group.pause() - - async def cmd_volume_set(self, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - await self.client.player.set_volume(volume_level) - # sync volume level with airplay player - if airplay := self.get_linked_airplay_player(False): - if airplay.state not in (PlayerState.PLAYING, PlayerState.PAUSED): - airplay.volume_level = volume_level - - async def cmd_volume_mute(self, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - await self.client.player.set_volume(muted=muted) - - def update_attributes(self) -> None: # noqa: PLR0915 - """Update the player attributes.""" - if not self.mass_player: - return - self.mass_player.available = self.connected - if not self.connected: - return - if self.client.player.has_fixed_volume: - self.mass_player.volume_level = 100 - else: - self.mass_player.volume_level = self.client.player.volume_level or 0 - self.mass_player.volume_muted = self.client.player.volume_muted - - group_parent = None - if self.client.player.is_coordinator: - # player is group coordinator - active_group = self.client.player.group - self.mass_player.group_childs = ( - set(self.client.player.group_members) - if len(self.client.player.group_members) > 1 - else set() - ) - self.mass_player.synced_to = None - else: - # player is group child (synced to another player) - group_parent = self.prov.sonos_players.get(self.client.player.group.coordinator_id) - if not group_parent or not group_parent.client or not group_parent.client.player: - # handle race condition where the group parent is not yet discovered - return - active_group = group_parent.client.player.group - self.mass_player.group_childs = set() - self.mass_player.synced_to = active_group.coordinator_id - self.mass_player.active_source = active_group.coordinator_id - - if airplay := self.get_linked_airplay_player(True): - # linked airplay player is active, update media from there - self.mass_player.state = airplay.state - self.mass_player.powered = airplay.powered - self.mass_player.active_source = airplay.active_source - self.mass_player.elapsed_time = airplay.elapsed_time - self.mass_player.elapsed_time_last_updated = airplay.elapsed_time_last_updated - # mark 'next_previous' feature as unsupported when airplay mode is active - if PlayerFeature.NEXT_PREVIOUS in self.mass_player.supported_features: - self.mass_player.supported_features = ( - x - for x in self.mass_player.supported_features - if x != PlayerFeature.NEXT_PREVIOUS - ) - return - # ensure 'next_previous' feature is supported when airplay mode is not active - if PlayerFeature.NEXT_PREVIOUS not in self.mass_player.supported_features: - self.mass_player.supported_features = ( - *self.mass_player.supported_features, - PlayerFeature.NEXT_PREVIOUS, - ) - - # map playback state - self.mass_player.state = PLAYBACK_STATE_MAP[active_group.playback_state] - self.mass_player.elapsed_time = active_group.position - - # figure out the active source based on the container - container_type = active_group.container_type - active_service = active_group.active_service - container = active_group.playback_metadata.get("container") - if container_type == ContainerType.LINEIN: - self.mass_player.active_source = SOURCE_LINE_IN - elif container_type == ContainerType.AIRPLAY: - # check if the MA airplay player is active - airplay_player = self.mass.players.get(self.airplay_player_id) - if airplay_player and airplay_player.state in ( - PlayerState.PLAYING, - PlayerState.PAUSED, - ): - self.mass_player.active_source = airplay_player.active_source - else: - self.mass_player.active_source = SOURCE_AIRPLAY - elif container_type == ContainerType.STATION: - self.mass_player.active_source = SOURCE_RADIO - elif active_service == MusicService.SPOTIFY: - self.mass_player.active_source = SOURCE_SPOTIFY - elif active_service == MusicService.MUSIC_ASSISTANT: - if object_id := container.get("id", {}).get("objectId"): - self.mass_player.active_source = object_id.split(":")[-1] - else: - # its playing some service we did not yet map - self.mass_player.active_source = active_service - - # sonos has this weirdness that it maps idle to paused - # which is annoying to figure out if we want to resume or let - # MA back in control again. So for now, we just map it to idle here. - if ( - self.mass_player.state == PlayerState.PAUSED - and active_service != MusicService.MUSIC_ASSISTANT - ): - self.mass_player.state = PlayerState.IDLE - - # parse current media - self.mass_player.elapsed_time = self.client.player.group.position - self.mass_player.elapsed_time_last_updated = time.time() - current_media = None - if (current_item := active_group.playback_metadata.get("currentItem")) and ( - (track := current_item.get("track")) and track.get("name") - ): - track_images = track.get("images", []) - track_image_url = track_images[0].get("url") if track_images else None - track_duration_millis = track.get("durationMillis") - current_media = PlayerMedia( - uri=track.get("id", {}).get("objectId") or track.get("mediaUrl"), - title=track["name"], - artist=track.get("artist", {}).get("name"), - album=track.get("album", {}).get("name"), - duration=track_duration_millis / 1000 if track_duration_millis else None, - image_url=track_image_url, - ) - if active_service == MusicService.MUSIC_ASSISTANT: - current_media.queue_id = self.mass_player.active_source - current_media.queue_item_id = current_item["id"] - # radio stream info - if container and container.get("name") and active_group.playback_metadata.get("streamInfo"): - images = container.get("images", []) - image_url = images[0].get("url") if images else None - current_media = PlayerMedia( - uri=container.get("id", {}).get("objectId"), - title=active_group.playback_metadata["streamInfo"], - album=container["name"], - image_url=image_url, - ) - # generic info from container (also when MA is playing!) - if container and container.get("name") and container.get("id"): - if not current_media: - current_media = PlayerMedia(container["id"]["objectId"]) - if not current_media.image_url: - images = container.get("images", []) - current_media.image_url = images[0].get("url") if images else None - if not current_media.title: - current_media.title = container["name"] - if not current_media.uri: - current_media.uri = container["id"]["objectId"] - - self.mass_player.current_media = current_media - - async def _connect(self, retry_on_fail: int = 0) -> None: - """Connect to the Sonos player.""" - if self._listen_task and not self._listen_task.done(): - self.logger.debug("Already connected to Sonos player: %s", self.player_id) - return - try: - await self.client.connect() - except (ConnectionFailed, ClientConnectorError) as err: - self.logger.warning("Failed to connect to Sonos player: %s", err) - self.mass_player.available = False - self.mass.players.update(self.player_id) - if not retry_on_fail: - raise - self.reconnect(min(retry_on_fail + 30, 3600)) - return - self.connected = True - self.logger.debug("Connected to player API") - init_ready = asyncio.Event() - - async def _listener() -> None: - try: - await self.client.start_listening(init_ready) - except Exception as err: - if not isinstance(err, ConnectionFailed | asyncio.CancelledError): - self.logger.exception("Error in Sonos player listener: %s", err) - finally: - self.logger.info("Disconnected from player API") - if self.connected: - # we didn't explicitly disconnect, try to reconnect - # this should simply try to reconnect once and if that fails - # we rely on mdns to pick it up again later - await self._disconnect() - self.mass_player.available = False - self.mass.players.update(self.player_id) - self.reconnect(5) - - self._listen_task = asyncio.create_task(_listener()) - await init_ready.wait() - - async def _disconnect(self) -> None: - """Disconnect the client and cleanup.""" - self.connected = False - if self._listen_task and not self._listen_task.done(): - self._listen_task.cancel() - if self.client: - await self.client.disconnect() - self.logger.debug("Disconnected from player API") - - def _on_player_event(self, event: SonosEvent) -> None: - """Handle incoming event from player.""" - self.update_attributes() - self.mass.players.update(self.player_id) - - def _on_airplay_player_event(self, event: MassEvent) -> None: - """Handle incoming event from linked airplay player.""" - if not self.mass.config.get_raw_player_config_value(self.player_id, CONF_AIRPLAY_MODE): - return - if event.object_id != self.airplay_player_id: - return - self.update_attributes() - self.mass.players.update(self.player_id) - - async def _on_mass_queue_items_event(self, event: MassEvent) -> None: - """Handle incoming event from linked MA playerqueue.""" - # If the queue items changed and we have an active sonos queue, - # we need to inform the sonos queue to refresh the items. - if self.mass_player.active_source != event.object_id: - return - if not self.connected: - return - queue = self.mass.player_queues.get(event.object_id) - if not queue or queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): - return - if session_id := self.client.player.group.active_session_id: - await self.client.api.playback_session.refresh_cloud_queue(session_id) - - async def _on_mass_queue_event(self, event: MassEvent) -> None: - """Handle incoming event from linked MA playerqueue.""" - if self.mass_player.active_source != event.object_id: - return - if not self.connected: - return - # sync crossfade and repeat modes - queue = self.mass.player_queues.get(event.object_id) - if not queue or queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED): - return - crossfade = await self.mass.config.get_player_config_value(queue.queue_id, CONF_CROSSFADE) - repeat_single_enabled = queue.repeat_mode == RepeatMode.ONE - repeat_all_enabled = queue.repeat_mode == RepeatMode.ALL - play_modes = self.client.player.group.play_modes - if ( - play_modes.crossfade != crossfade - or play_modes.repeat != repeat_all_enabled - or play_modes.repeat_one != repeat_single_enabled - ): - await self.client.player.group.set_play_modes( - crossfade=crossfade, repeat=repeat_all_enabled, repeat_one=repeat_single_enabled - ) diff --git a/music_assistant/server/providers/sonos/provider.py b/music_assistant/server/providers/sonos/provider.py deleted file mode 100644 index 2e95f68b..00000000 --- a/music_assistant/server/providers/sonos/provider.py +++ /dev/null @@ -1,513 +0,0 @@ -""" -Sonos Player provider for Music Assistant for speakers running the S2 firmware. - -Based on the aiosonos library, which leverages the new websockets API of the Sonos S2 firmware. -https://github.com/music-assistant/aiosonos -""" - -from __future__ import annotations - -import asyncio -import time -from typing import TYPE_CHECKING - -import shortuuid -from aiohttp import web -from aiohttp.client_exceptions import ClientError -from aiosonos.api.models import SonosCapability -from aiosonos.utils import get_discovery_info -from zeroconf import ServiceStateChange - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, - ConfigEntry, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ConfigEntryType, ContentType, ProviderFeature -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, PlayerMedia -from music_assistant.constants import MASS_LOGO_ONLINE, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.models.player_provider import PlayerProvider - -from .const import CONF_AIRPLAY_MODE -from .helpers import get_primary_ip_address -from .player import SonosPlayer - -if TYPE_CHECKING: - from zeroconf.asyncio import AsyncServiceInfo - - -class SonosPlayerProvider(PlayerProvider): - """Sonos Player provider.""" - - sonos_players: dict[str, SonosPlayer] - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.sonos_players: dict[str, SonosPlayer] = {} - self.mass.streams.register_dynamic_route( - "/sonos_queue/v2.3/itemWindow", self._handle_sonos_queue_itemwindow - ) - self.mass.streams.register_dynamic_route( - "/sonos_queue/v2.3/version", self._handle_sonos_queue_version - ) - self.mass.streams.register_dynamic_route( - "/sonos_queue/v2.3/context", self._handle_sonos_queue_context - ) - self.mass.streams.register_dynamic_route( - "/sonos_queue/v2.3/timePlayed", self._handle_sonos_queue_time_played - ) - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - # disconnect all players - await asyncio.gather(*(player.unload() for player in self.sonos_players.values())) - self.sonos_players = None - self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/itemWindow") - self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/version") - self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/context") - self.mass.streams.unregister_dynamic_route("/sonos_queue/v2.3/timePlayed") - - async def on_mdns_service_state_change( - self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None - ) -> None: - """Handle MDNS service state callback.""" - if state_change == ServiceStateChange.Removed: - # we don't listen for removed players here. - # instead we just wait for the player connection to fail - return - if "uuid" not in info.decoded_properties: - # not a S2 player - return - name = name.split("@", 1)[1] if "@" in name else name - player_id = info.decoded_properties["uuid"] - # handle update for existing device - if sonos_player := self.sonos_players.get(player_id): - if mass_player := sonos_player.mass_player: - cur_address = get_primary_ip_address(info) - if cur_address and cur_address != sonos_player.ip_address: - sonos_player.logger.debug( - "Address updated from %s to %s", sonos_player.ip_address, cur_address - ) - sonos_player.ip_address = cur_address - mass_player.device_info = DeviceInfo( - model=mass_player.device_info.model, - manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), - ) - if not sonos_player.connected: - self.logger.debug("Player back online: %s", mass_player.display_name) - sonos_player.client.player_ip = cur_address - # schedule reconnect - sonos_player.reconnect() - self.mass.players.update(player_id) - return - # handle new player setup in a delayed task because mdns announcements - # can arrive in (duplicated) bursts - task_id = f"setup_sonos_{player_id}" - self.mass.call_later(5, self._setup_player, player_id, name, info, task_id=task_id) - - async def get_player_config_entries( - self, - player_id: str, - ) -> tuple[ConfigEntry, ...]: - """Return Config Entries for the given player.""" - base_entries = ( - *await super().get_player_config_entries(player_id), - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, - CONF_ENTRY_ENFORCE_MP3, - create_sample_rates_config_entry(48000, 24, 48000, 24, True), - ) - if not (sonos_player := self.sonos_players.get(player_id)): - # most probably the player is not yet discovered - return base_entries - return ( - *base_entries, - ConfigEntry( - key=CONF_AIRPLAY_MODE, - type=ConfigEntryType.BOOLEAN, - label="Enable Airplay mode (experimental)", - description="Almost all newer Sonos speakers have Airplay support. " - "If you have the Airplay provider enabled in Music Assistant, " - "your Sonos speakers will also be detected as Airplay speakers, meaning " - "you can group them with other Airplay speakers.\n\n" - "By default, Music Assistant uses the Sonos protocol for playback but with this " - "feature enabled, it will use the Airplay protocol instead by redirecting " - "the playback related commands to the linked Airplay player in Music Assistant, " - "allowing you to mix and match Sonos speakers with Airplay speakers. \n\n" - "TIP: When this feature is enabled, it make sense to set the underlying airplay " - "players to hide in the UI in the player settings to prevent duplicate players.", - required=False, - default_value=False, - hidden=SonosCapability.AIRPLAY - not in sonos_player.discovery_info["device"]["capabilities"], - ), - ) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.cmd_stop() - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.cmd_play() - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.cmd_pause() - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.cmd_volume_set(volume_level) - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.cmd_volume_mute(muted) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - await self.cmd_sync_many(target_player, [player_id]) - - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - sonos_player = self.sonos_players[target_player] - await sonos_player.client.player.group.modify_group_members( - player_ids_to_add=child_player_ids, player_ids_to_remove=[] - ) - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - sonos_player = self.sonos_players[player_id] - await sonos_player.client.player.leave_group() - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - sonos_player = self.sonos_players[player_id] - sonos_player.queue_version = shortuuid.random(8) - mass_player = self.mass.players.get(player_id) - if sonos_player.client.player.is_passive: - # this should be already handled by the player manager, but just in case... - msg = ( - f"Player {mass_player.display_name} can not " - "accept play_media command, it is synced to another player." - ) - raise PlayerCommandFailed(msg) - # for now always reset the active session - sonos_player.client.player.group.active_session_id = None - if airplay := sonos_player.get_linked_airplay_player(True): - # linked airplay player is active, redirect the command - self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.") - mass_player.active_source = airplay.active_source - # Sonos has an annoying bug (for years already, and they dont seem to care), - # where it looses its sync childs when airplay playback is (re)started. - # Try to handle it here with this workaround. - group_childs = ( - sonos_player.client.player.group_members - if len(sonos_player.client.player.group_members) > 1 - else [] - ) - if group_childs: - await self.mass.players.cmd_unsync_many(group_childs) - await self.mass.players.play_media(airplay.player_id, media) - if group_childs: - self.mass.call_later(5, self.cmd_sync_many, player_id, group_childs) - return - - if media.queue_id and media.queue_id.startswith("ugp_"): - # Special UGP stream - handle with play URL - await sonos_player.client.player.group.play_stream_url(media.uri, None) - return - - if media.queue_id: - # create a sonos cloud queue and load it - cloud_queue_url = f"{self.mass.streams.base_url}/sonos_queue/v2.3/" - await sonos_player.client.player.group.play_cloud_queue( - cloud_queue_url, - http_authorization=media.queue_id, - item_id=media.queue_item_id, - queue_version=sonos_player.queue_version, - ) - return - - # play a single uri/url - # note that this most probably will only work for (long running) radio streams - if self.mass.config.get_raw_player_config_value( - player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value - ): - media.uri = media.uri.replace(".flac", ".mp3") - await sonos_player.client.player.group.play_stream_url( - media.uri, {"name": media.title, "type": "track"} - ) - - async def cmd_next(self, player_id: str) -> None: - """Handle NEXT TRACK command for given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.client.player.group.skip_to_next_track() - - async def cmd_previous(self, player_id: str) -> None: - """Handle PREVIOUS TRACK command for given player.""" - if sonos_player := self.sonos_players[player_id]: - await sonos_player.client.player.group.skip_to_previous_track() - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next queue item on the player.""" - # We do nothing here as we handle the queue in the cloud queue endpoint. - # For sonos s2, instead of enqueuing tracks one by one, the sonos player itself - # can interact with our queue directly through the cloud queue endpoint. - - async def play_announcement( - self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None - ) -> None: - """Handle (provider native) playback of an announcement on given player.""" - sonos_player = self.sonos_players[player_id] - self.logger.debug( - "Playing announcement %s on %s", - announcement.uri, - sonos_player.mass_player.display_name, - ) - volume_level = self.mass.players.get_announcement_volume(player_id, volume_level) - await sonos_player.client.player.play_audio_clip( - announcement.uri, volume_level, name="Announcement" - ) - # Wait until the announcement is finished playing - # This is helpful for people who want to play announcements in a sequence - # yeah we can also setup a subscription on the sonos player for this, but this is easier - media_info = await parse_tags(announcement.uri) - duration = media_info.duration or 10 - await asyncio.sleep(duration) - - async def _setup_player(self, player_id: str, name: str, info: AsyncServiceInfo) -> None: - """Handle setup of a new player that is discovered using mdns.""" - assert player_id not in self.sonos_players - address = get_primary_ip_address(info) - if address is None: - return - if not self.mass.config.get_raw_player_config_value(player_id, "enabled", True): - self.logger.debug("Ignoring %s in discovery as it is disabled.", name) - return - try: - discovery_info = await get_discovery_info(self.mass.http_session, address) - except ClientError as err: - self.logger.debug("Ignoring %s in discovery as it is not reachable: %s", name, str(err)) - return - display_name = discovery_info["device"].get("name") or name - if SonosCapability.PLAYBACK not in discovery_info["device"]["capabilities"]: - # this will happen for satellite speakers in a surround/stereo setup - self.logger.debug( - "Ignoring %s in discovery as it is a passive satellite.", display_name - ) - return - self.logger.debug("Discovered Sonos device %s on %s", name, address) - self.sonos_players[player_id] = sonos_player = SonosPlayer( - self, player_id, discovery_info=discovery_info, ip_address=address - ) - await sonos_player.setup() - - async def _handle_sonos_queue_itemwindow(self, request: web.Request) -> web.Response: - """ - Handle the Sonos CloudQueue ItemWindow endpoint. - - https://docs.sonos.com/reference/itemwindow - """ - self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue ItemWindow request: %s", request.query) - sonos_playback_id = request.headers["X-Sonos-Playback-Id"] - sonos_player_id = sonos_playback_id.split(":")[0] - upcoming_window_size = int(request.query.get("upcomingWindowSize") or 10) - previous_window_size = int(request.query.get("previousWindowSize") or 10) - queue_version = request.query.get("queueVersion") - context_version = request.query.get("contextVersion") - if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)): - return web.Response(status=501) - if item_id := request.query.get("itemId"): - queue_index = self.mass.player_queues.index_by_id(mass_queue.queue_id, item_id) - else: - queue_index = mass_queue.current_index - if queue_index is None: - return web.Response(status=501) - offset = max(queue_index - previous_window_size, 0) - queue_items = self.mass.player_queues.items( - mass_queue.queue_id, - limit=upcoming_window_size + previous_window_size, - offset=max(queue_index - previous_window_size, 0), - ) - enforce_mp3 = self.mass.config.get_raw_player_config_value( - sonos_player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value - ) - sonos_queue_items = [ - { - "id": item.queue_item_id, - "deleted": not item.media_item.available, - "policies": {}, - "track": { - "type": "track", - "mediaUrl": self.mass.streams.resolve_stream_url( - item, output_codec=ContentType.MP3 if enforce_mp3 else ContentType.FLAC - ), - "contentType": "audio/flac", - "service": { - "name": "Music Assistant", - "id": "8", - "accountId": "", - "objectId": item.queue_item_id, - }, - "name": item.name, - "imageUrl": self.mass.metadata.get_image_url( - item.image, prefer_proxy=False, image_format="jpeg" - ) - if item.image - else None, - "durationMillis": item.duration * 1000 if item.duration else None, - "artist": { - "name": artist_str, - } - if item.media_item - and (artist_str := getattr(item.media_item, "artist_str", None)) - else None, - "album": { - "name": album.name, - } - if item.media_item and (album := getattr(item.media_item, "album", None)) - else None, - "quality": { - "bitDepth": item.streamdetails.audio_format.bit_depth, - "sampleRate": item.streamdetails.audio_format.sample_rate, - "codec": item.streamdetails.audio_format.content_type.value, - "lossless": item.streamdetails.audio_format.content_type.is_lossless(), - } - if item.streamdetails - else None, - }, - } - for item in queue_items - ] - result = { - "includesBeginningOfQueue": offset == 0, - "includesEndOfQueue": mass_queue.items <= (queue_index + len(sonos_queue_items)), - "contextVersion": context_version, - "queueVersion": queue_version, - "items": sonos_queue_items, - } - return web.json_response(result) - - async def _handle_sonos_queue_version(self, request: web.Request) -> web.Response: - """ - Handle the Sonos CloudQueue Version endpoint. - - https://docs.sonos.com/reference/version - """ - self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue Version request: %s", request.query) - sonos_playback_id = request.headers["X-Sonos-Playback-Id"] - sonos_player_id = sonos_playback_id.split(":")[0] - if not (sonos_player := self.sonos_players.get(sonos_player_id)): - return web.Response(status=501) - context_version = request.query.get("contextVersion") or "1" - queue_version = sonos_player.queue_version - result = {"contextVersion": context_version, "queueVersion": queue_version} - return web.json_response(result) - - async def _handle_sonos_queue_context(self, request: web.Request) -> web.Response: - """ - Handle the Sonos CloudQueue Context endpoint. - - https://docs.sonos.com/reference/context - """ - self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue Context request: %s", request.query) - sonos_playback_id = request.headers["X-Sonos-Playback-Id"] - sonos_player_id = sonos_playback_id.split(":")[0] - if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)): - return web.Response(status=501) - if not (sonos_player := self.sonos_players.get(sonos_player_id)): - return web.Response(status=501) - result = { - "contextVersion": "1", - "queueVersion": sonos_player.queue_version, - "container": { - "type": "playlist", - "name": "Music Assistant", - "imageUrl": MASS_LOGO_ONLINE, - "service": {"name": "Music Assistant", "id": "mass"}, - "id": { - "serviceId": "mass", - "objectId": f"mass:queue:{mass_queue.queue_id}", - "accountId": "", - }, - }, - "reports": { - "sendUpdateAfterMillis": 0, - "periodicIntervalMillis": 10000, - "sendPlaybackActions": True, - }, - "playbackPolicies": { - "canSkip": True, - "limitedSkips": False, - "canSkipToItem": True, - "canSkipBack": True, - "canSeek": False, # somehow not working correctly, investigate later - "canRepeat": True, - "canRepeatOne": True, - "canCrossfade": True, - "canShuffle": False, # handled by our queue controller itself - "showNNextTracks": 5, - "showNPreviousTracks": 5, - }, - } - return web.json_response(result) - - async def _handle_sonos_queue_time_played(self, request: web.Request) -> web.Response: - """ - Handle the Sonos CloudQueue TimePlayed endpoint. - - https://docs.sonos.com/reference/timeplayed - """ - self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue TimePlayed request: %s", request.query) - json_body = await request.json() - sonos_playback_id = request.headers["X-Sonos-Playback-Id"] - sonos_player_id = sonos_playback_id.split(":")[0] - if not (mass_player := self.mass.players.get(sonos_player_id)): - return web.Response(status=501) - if not (sonos_player := self.sonos_players.get(sonos_player_id)): - return web.Response(status=501) - for item in json_body["items"]: - if item["queueVersion"] != sonos_player.queue_version: - continue - if item["type"] != "update": - continue - if "positionMillis" not in item: - continue - mass_player.current_media = PlayerMedia( - uri=item["mediaUrl"], queue_id=sonos_playback_id, queue_item_id=item["id"] - ) - mass_player.elapsed_time = item["positionMillis"] / 1000 - mass_player.elapsed_time_last_updated = time.time() - self.mass.players.update(sonos_player_id) - break - return web.Response(status=204) diff --git a/music_assistant/server/providers/sonos_s1/__init__.py b/music_assistant/server/providers/sonos_s1/__init__.py deleted file mode 100644 index e16042fa..00000000 --- a/music_assistant/server/providers/sonos_s1/__init__.py +++ /dev/null @@ -1,487 +0,0 @@ -""" -Sonos Player S1 provider for Music Assistant. - -Based on the SoCo library for Sonos which uses the legacy/V1 UPnP API. - -Note that large parts of this code are copied over from the Home Assistant -integration for Sonos. -""" - -from __future__ import annotations - -import asyncio -import logging -from collections import OrderedDict -from dataclasses import dataclass, field -from typing import TYPE_CHECKING - -from requests.exceptions import RequestException -from soco import config as soco_config -from soco import events_asyncio, zonegroupstate -from soco.discovery import discover, scan_network - -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, - CONF_ENTRY_HTTP_PROFILE_FORCED_1, - ConfigEntry, - ConfigValueType, - create_sample_rates_config_entry, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.errors import PlayerCommandFailed, PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import CONF_CROSSFADE, CONF_ENFORCE_MP3, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.didl_lite import create_didl_metadata -from music_assistant.server.models.player_provider import PlayerProvider - -from .player import SonosPlayer - -if TYPE_CHECKING: - from soco.core import SoCo - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -PLAYER_FEATURES = ( - PlayerFeature.SYNC, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.PAUSE, - PlayerFeature.ENQUEUE, -) - -CONF_NETWORK_SCAN = "network_scan" -CONF_HOUSEHOLD_ID = "household_id" -SUBSCRIPTION_TIMEOUT = 1200 -ZGS_SUBSCRIPTION_TIMEOUT = 2 - -CONF_ENTRY_SAMPLE_RATES = create_sample_rates_config_entry(48000, 16, 48000, 16, True) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - soco_config.EVENTS_MODULE = events_asyncio - zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT - prov = SonosPlayerProvider(mass, manifest, config) - # set-up soco logging - if prov.logger.isEnabledFor(VERBOSE_LOG_LEVEL): - logging.getLogger("soco").setLevel(logging.DEBUG) - else: - logging.getLogger("soco").setLevel(prov.logger.level + 10) - await prov.handle_async_init() - return prov - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - household_ids = await discover_household_ids(mass) - return ( - ConfigEntry( - key=CONF_NETWORK_SCAN, - type=ConfigEntryType.BOOLEAN, - label="Enable network scan for discovery", - default_value=False, - description="Enable network scan for discovery of players. \n" - "Can be used if (some of) your players are not automatically discovered.\n" - "Should normally not be needed", - ), - ConfigEntry( - key=CONF_HOUSEHOLD_ID, - type=ConfigEntryType.STRING, - label="Household ID", - default_value=household_ids[0] if household_ids else None, - description="Household ID for the Sonos (S1) system. Will be auto detected if empty.", - category="advanced", - required=False, - ), - ) - - -@dataclass -class UnjoinData: - """Class to track data necessary for unjoin coalescing.""" - - players: list[SonosPlayer] - event: asyncio.Event = field(default_factory=asyncio.Event) - - -class SonosPlayerProvider(PlayerProvider): - """Sonos Player provider.""" - - sonosplayers: dict[str, SonosPlayer] | None = None - _discovery_running: bool = False - _discovery_reschedule_timer: asyncio.TimerHandle | None = None - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.sonosplayers: OrderedDict[str, SonosPlayer] = OrderedDict() - self.topology_condition = asyncio.Condition() - self.boot_counts: dict[str, int] = {} - self.mdns_names: dict[str, str] = {} - self.unjoin_data: dict[str, UnjoinData] = {} - self._discovery_running = False - self.hosts_in_error: dict[str, bool] = {} - self.discovery_lock = asyncio.Lock() - self.creation_lock = asyncio.Lock() - self._known_invisible: set[SoCo] = set() - - async def unload(self) -> None: - """Handle close/cleanup of the provider.""" - if self._discovery_reschedule_timer: - self._discovery_reschedule_timer.cancel() - self._discovery_reschedule_timer = None - # await any in-progress discovery - while self._discovery_running: - await asyncio.sleep(0.5) - await asyncio.gather(*(player.offline() for player in self.sonosplayers.values())) - if events_asyncio.event_listener: - await events_asyncio.event_listener.async_stop() - self.sonosplayers = None - - async def get_player_config_entries( - self, - player_id: str, - ) -> tuple[ConfigEntry, ...]: - """Return Config Entries for the given player.""" - base_entries = await super().get_player_config_entries(player_id) - if not (self.sonosplayers.get(player_id)): - # most probably a syncgroup - return ( - *base_entries, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, - ) - return ( - *base_entries, - CONF_ENTRY_CROSSFADE, - CONF_ENTRY_SAMPLE_RATES, - CONF_ENTRY_ENFORCE_MP3, - CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, - CONF_ENTRY_HTTP_PROFILE_FORCED_1, - ) - - def is_device_invisible(self, ip_address: str) -> bool: - """Check if device at provided IP is known to be invisible.""" - return any(x for x in self._known_invisible if x.ip_address == ip_address) - - async def cmd_stop(self, player_id: str) -> None: - """Send STOP command to given player.""" - sonos_player = self.sonosplayers[player_id] - if sonos_player.sync_coordinator: - self.logger.debug( - "Ignore STOP command for %s: Player is synced to another player.", - player_id, - ) - return - await asyncio.to_thread(sonos_player.soco.stop) - self.mass.call_later(2, sonos_player.poll_speaker) - - async def cmd_play(self, player_id: str) -> None: - """Send PLAY command to given player.""" - sonos_player = self.sonosplayers[player_id] - if sonos_player.sync_coordinator: - self.logger.debug( - "Ignore PLAY command for %s: Player is synced to another player.", - player_id, - ) - return - await asyncio.to_thread(sonos_player.soco.play) - sonos_player.mass_player.poll_interval = 5 - self.mass.call_later(2, sonos_player.poll_speaker) - - async def cmd_pause(self, player_id: str) -> None: - """Send PAUSE command to given player.""" - sonos_player = self.sonosplayers[player_id] - if sonos_player.sync_coordinator: - self.logger.debug( - "Ignore PLAY command for %s: Player is synced to another player.", - player_id, - ) - return - if "Pause" not in sonos_player.soco.available_actions: - # pause not possible - await self.cmd_stop(player_id) - return - await asyncio.to_thread(sonos_player.soco.pause) - self.mass.call_later(2, sonos_player.poll_speaker) - - async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME_SET command to given player.""" - sonos_player = self.sonosplayers[player_id] - - def set_volume_level(player_id: str, volume_level: int) -> None: - sonos_player.soco.volume = volume_level - - await asyncio.to_thread(set_volume_level, player_id, volume_level) - self.mass.call_later(2, sonos_player.poll_speaker) - - async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - - def set_volume_mute(player_id: str, muted: bool) -> None: - sonos_player = self.sonosplayers[player_id] - sonos_player.soco.mute = muted - - await asyncio.to_thread(set_volume_mute, player_id, muted) - - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - sonos_player = self.sonosplayers[player_id] - sonos_master_player = self.sonosplayers[target_player] - await sonos_master_player.join([sonos_player]) - self.mass.call_later(2, sonos_player.poll_speaker) - - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - - - player_id: player_id of the player to handle the command. - """ - sonos_player = self.sonosplayers[player_id] - await sonos_player.unjoin() - self.mass.call_later(2, sonos_player.poll_speaker) - - async def play_media( - self, - player_id: str, - media: PlayerMedia, - ) -> None: - """Handle PLAY MEDIA on given player.""" - sonos_player = self.sonosplayers[player_id] - mass_player = self.mass.players.get(player_id) - if sonos_player.sync_coordinator: - # this should be already handled by the player manager, but just in case... - msg = ( - f"Player {mass_player.display_name} can not " - "accept play_media command, it is synced to another player." - ) - raise PlayerCommandFailed(msg) - if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3): - media.uri = media.uri.replace(".flac", ".mp3") - didl_metadata = create_didl_metadata(media) - await asyncio.to_thread(sonos_player.soco.play_uri, media.uri, meta=didl_metadata) - self.mass.call_later(2, sonos_player.poll_speaker) - sonos_player.mass_player.poll_interval = 5 - - async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: - """Handle enqueuing of the next queue item on the player.""" - sonos_player = self.sonosplayers[player_id] - if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3): - media.uri = media.uri.replace(".flac", ".mp3") - didl_metadata = create_didl_metadata(media) - # set crossfade according to player setting - crossfade = await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) - if sonos_player.crossfade != crossfade: - - def set_crossfade() -> None: - try: - sonos_player.soco.cross_fade = crossfade - sonos_player.crossfade = crossfade - except Exception as err: - self.logger.warning( - "Unable to set crossfade for player %s: %s", sonos_player.zone_name, err - ) - - await asyncio.to_thread(set_crossfade) - - try: - await asyncio.to_thread( - sonos_player.soco.avTransport.SetNextAVTransportURI, - [("InstanceID", 0), ("NextURI", media.uri), ("NextURIMetaData", didl_metadata)], - timeout=60, - ) - except Exception as err: - self.logger.warning( - "Unable to enqueue next track on player: %s: %s", sonos_player.zone_name, err - ) - else: - self.logger.debug( - "Enqued next track (%s) to player %s", - media.title or media.uri, - sonos_player.soco.player_name, - ) - - async def poll_player(self, player_id: str) -> None: - """Poll player for state updates.""" - if player_id not in self.sonosplayers: - return - sonos_player = self.sonosplayers[player_id] - # dynamically change the poll interval - if sonos_player.mass_player.state == PlayerState.PLAYING: - sonos_player.mass_player.poll_interval = 5 - elif sonos_player.mass_player.powered: - sonos_player.mass_player.poll_interval = 20 - else: - sonos_player.mass_player.poll_interval = 60 - try: - # the check_poll logic will work out what endpoints need polling now - # based on when we last received info from the device - if needs_poll := await sonos_player.check_poll(): - await sonos_player.poll_speaker() - # always update the attributes - sonos_player.update_player(signal_update=needs_poll) - except ConnectionResetError as err: - raise PlayerUnavailableError from err - - async def discover_players(self) -> None: - """Discover Sonos players on the network.""" - if self._discovery_running: - return - - allow_network_scan = self.config.get_value(CONF_NETWORK_SCAN) - if not (household_id := self.config.get_value(CONF_HOUSEHOLD_ID)): - household_id = "Sonos" - - def do_discover() -> None: - """Run discovery and add players in executor thread.""" - self._discovery_running = True - try: - self.logger.debug("Sonos discovery started...") - discovered_devices: set[SoCo] = discover( - timeout=30, household_id=household_id, allow_network_scan=allow_network_scan - ) - if discovered_devices is None: - discovered_devices = set() - # process new players - for soco in discovered_devices: - try: - self._add_player(soco) - except RequestException as err: - # player is offline - self.logger.debug("Failed to add SonosPlayer %s: %s", soco, err) - except Exception as err: - self.logger.warning( - "Failed to add SonosPlayer %s: %s", - soco, - err, - exc_info=err if self.logger.isEnabledFor(10) else None, - ) - finally: - self._discovery_running = False - - await self.mass.create_task(do_discover) - - def reschedule() -> None: - self._discovery_reschedule_timer = None - self.mass.create_task(self.discover_players()) - - # reschedule self once finished - self._discovery_reschedule_timer = self.mass.loop.call_later(1800, reschedule) - - def _add_player(self, soco: SoCo) -> None: - """Add discovered Sonos player.""" - player_id = soco.uid - # check if existing player changed IP - if existing := self.sonosplayers.get(player_id): - if existing.soco.ip_address != soco.ip_address: - existing.update_ip(soco.ip_address) - return - if not soco.is_visible: - return - enabled = self.mass.config.get_raw_player_config_value(player_id, "enabled", True) - if not enabled: - self.logger.debug("Ignoring disabled player: %s", player_id) - return - - speaker_info = soco.get_speaker_info(True, timeout=7) - if soco.uid not in self.boot_counts: - self.boot_counts[soco.uid] = soco.boot_seqnum - self.logger.debug("Adding new player: %s", speaker_info) - transport_info = soco.get_current_transport_info() - play_state = transport_info["current_transport_state"] - if not (mass_player := self.mass.players.get(soco.uid)): - mass_player = Player( - player_id=soco.uid, - provider=self.instance_id, - type=PlayerType.PLAYER, - name=soco.player_name, - available=True, - powered=play_state in ("PLAYING", "TRANSITIONING"), - supported_features=PLAYER_FEATURES, - device_info=DeviceInfo( - model=speaker_info["model_name"], - address=soco.ip_address, - manufacturer="SONOS", - ), - needs_poll=True, - poll_interval=30, - ) - self.sonosplayers[player_id] = sonos_player = SonosPlayer( - self, - soco=soco, - mass_player=mass_player, - ) - if not soco.fixed_volume: - mass_player.supported_features = ( - *mass_player.supported_features, - PlayerFeature.VOLUME_SET, - ) - asyncio.run_coroutine_threadsafe( - self.mass.players.register_or_update(sonos_player.mass_player), loop=self.mass.loop - ) - - -async def discover_household_ids(mass: MusicAssistant, prefer_s1: bool = True) -> list[str]: - """Discover the HouseHold ID of S1 speaker(s) the network.""" - if cache := await mass.cache.get("sonos_household_ids"): - return cache - household_ids: list[str] = [] - - def get_all_sonos_ips() -> set[SoCo]: - """Run full network discovery and return IP's of all devices found on the network.""" - discovered_zones: set[SoCo] | None - if discovered_zones := scan_network(multi_household=True): - return {zone.ip_address for zone in discovered_zones} - return set() - - all_sonos_ips = await asyncio.to_thread(get_all_sonos_ips) - for ip_address in all_sonos_ips: - async with mass.http_session.get(f"http://{ip_address}:1400/status/zp") as resp: - if resp.status == 200: - data = await resp.text() - if prefer_s1 and "2" in data: - continue - if "HouseholdControlID" in data: - household_id = data.split("")[1].split( - "" - )[0] - household_ids.append(household_id) - await mass.cache.set("sonos_household_ids", household_ids, 3600) - return household_ids diff --git a/music_assistant/server/providers/sonos_s1/helpers.py b/music_assistant/server/providers/sonos_s1/helpers.py deleted file mode 100644 index 662cf6c4..00000000 --- a/music_assistant/server/providers/sonos_s1/helpers.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Helper methods for common tasks.""" - -from __future__ import annotations - -import logging -from collections.abc import Callable -from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload - -from soco import SoCo -from soco.exceptions import SoCoException, SoCoUPnPException - -from music_assistant.common.models.errors import PlayerCommandFailed - -if TYPE_CHECKING: - from . import SonosPlayer - - -UID_PREFIX = "RINCON_" -UID_POSTFIX = "01400" - -_LOGGER = logging.getLogger(__name__) - -_T = TypeVar("_T", bound="SonosPlayer") -_R = TypeVar("_R") -_P = ParamSpec("_P") - -_FuncType = Callable[Concatenate[_T, _P], _R] -_ReturnFuncType = Callable[Concatenate[_T, _P], _R | None] - - -class SonosUpdateError(PlayerCommandFailed): - """Update failed.""" - - -@overload -def soco_error( - errorcodes: None = ..., -) -> Callable[[_FuncType[_T, _P, _R]], _FuncType[_T, _P, _R]]: ... - - -@overload -def soco_error( - errorcodes: list[str], -) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: ... - - -def soco_error( - errorcodes: list[str] | None = None, -) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: - """Filter out specified UPnP errors and raise exceptions for service calls.""" - - def decorator(funct: _FuncType[_T, _P, _R]) -> _ReturnFuncType[_T, _P, _R]: - """Decorate functions.""" - - def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: - """Wrap for all soco UPnP exception.""" - args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) - try: - result = funct(self, *args, **kwargs) - except (OSError, SoCoException, SoCoUPnPException, TimeoutError) as err: - error_code = getattr(err, "error_code", None) - function = funct.__qualname__ - if errorcodes and error_code in errorcodes: - _LOGGER.debug("Error code %s ignored in call to %s", error_code, function) - return None - - if (target := _find_target_identifier(self, args_soco)) is None: - msg = "Unexpected use of soco_error" - raise RuntimeError(msg) from err - - message = f"Error calling {function} on {target}: {err}" - raise SonosUpdateError(message) from err - - return result - - return wrapper - - return decorator - - -def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None: - """Extract the best available target identifier from the provided instance object.""" - if zone_name := getattr(instance, "zone_name", None): - # SonosPlayer instance - return zone_name - if soco := getattr(instance, "soco", fallback_soco): - # Holds a SoCo instance attribute - # Only use attributes with no I/O - return soco._player_name or soco.ip_address - return None - - -def hostname_to_uid(hostname: str) -> str: - """Convert a Sonos hostname to a uid.""" - if hostname.startswith("Sonos-"): - baseuid = hostname.removeprefix("Sonos-").replace(".local.", "") - elif hostname.startswith("sonos"): - baseuid = hostname.removeprefix("sonos").replace(".local.", "") - else: - msg = f"{hostname} is not a sonos device." - raise ValueError(msg) - return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" - - -def sync_get_visible_zones(soco: SoCo) -> set[SoCo]: - """Ensure I/O attributes are cached and return visible zones.""" - _ = soco.household_id - _ = soco.uid - return soco.visible_zones diff --git a/music_assistant/server/providers/sonos_s1/icon.png b/music_assistant/server/providers/sonos_s1/icon.png deleted file mode 100644 index be274bf0..00000000 Binary files a/music_assistant/server/providers/sonos_s1/icon.png and /dev/null differ diff --git a/music_assistant/server/providers/sonos_s1/icon.svg b/music_assistant/server/providers/sonos_s1/icon.svg deleted file mode 100644 index 60d9e677..00000000 --- a/music_assistant/server/providers/sonos_s1/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/sonos_s1/manifest.json b/music_assistant/server/providers/sonos_s1/manifest.json deleted file mode 100644 index b33c6ace..00000000 --- a/music_assistant/server/providers/sonos_s1/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "player", - "domain": "sonos_s1", - "name": "SONOS S1", - "description": "SONOS Player provider for Music Assistant for the S1 hardware, based on the Soco library. Select this provider if you have Sonos devices on the S1 operating system (with the S1 Controller app)", - "codeowners": [ - "@music-assistant" - ], - "requirements": [ - "soco==0.30.5", - "defusedxml==0.7.1" - ], - "documentation": "https://music-assistant.io/player-support/sonos/", - "multi_instance": false, - "builtin": false -} diff --git a/music_assistant/server/providers/sonos_s1/player.py b/music_assistant/server/providers/sonos_s1/player.py deleted file mode 100644 index 600e4970..00000000 --- a/music_assistant/server/providers/sonos_s1/player.py +++ /dev/null @@ -1,813 +0,0 @@ -""" -Sonos Player provider for Music Assistant: SonosPlayer object/model. - -Note that large parts of this code are copied over from the Home Assistant -integration for Sonos. -""" - -from __future__ import annotations - -import asyncio -import contextlib -import datetime -import logging -import time -from collections.abc import Callable, Coroutine -from typing import TYPE_CHECKING, Any - -from soco import SoCoException -from soco.core import ( - MUSIC_SRC_AIRPLAY, - MUSIC_SRC_LINE_IN, - MUSIC_SRC_RADIO, - MUSIC_SRC_SPOTIFY_CONNECT, - MUSIC_SRC_TV, - SoCo, -) -from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer - -from music_assistant.common.helpers.datetime import utc -from music_assistant.common.models.enums import PlayerFeature, PlayerState -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, Player -from music_assistant.constants import VERBOSE_LOG_LEVEL - -from .helpers import SonosUpdateError, soco_error - -if TYPE_CHECKING: - from soco.events_base import Event as SonosEvent - from soco.events_base import SubscriptionBase - - from . import SonosPlayerProvider - -CALLBACK_TYPE = Callable[[], None] -LOGGER = logging.getLogger(__name__) - -PLAYER_FEATURES = ( - PlayerFeature.SYNC, - PlayerFeature.VOLUME_MUTE, - PlayerFeature.VOLUME_SET, -) -DURATION_SECONDS = "duration_in_s" -POSITION_SECONDS = "position_in_s" -SUBSCRIPTION_TIMEOUT = 1200 -ZGS_SUBSCRIPTION_TIMEOUT = 2 -AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) -AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 -SONOS_STATE_PLAYING = "PLAYING" -SONOS_STATE_TRANSITIONING = "TRANSITIONING" -NEVER_TIME = -1200.0 -RESUB_COOLDOWN_SECONDS = 10.0 -SUBSCRIPTION_SERVICES = { - # "alarmClock", - "avTransport", - # "contentDirectory", - "deviceProperties", - "renderingControl", - "zoneGroupTopology", -} -SUPPORTED_VANISH_REASONS = ("powered off", "sleeping", "switch to bluetooth", "upgrade") -UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] -LINEIN_SOURCES = (MUSIC_SRC_TV, MUSIC_SRC_LINE_IN) -SOURCE_AIRPLAY = "AirPlay" -SOURCE_LINEIN = "Line-in" -SOURCE_SPOTIFY_CONNECT = "Spotify Connect" -SOURCE_TV = "TV" -SOURCE_MAPPING = { - MUSIC_SRC_AIRPLAY: SOURCE_AIRPLAY, - MUSIC_SRC_TV: SOURCE_TV, - MUSIC_SRC_LINE_IN: SOURCE_LINEIN, - MUSIC_SRC_SPOTIFY_CONNECT: SOURCE_SPOTIFY_CONNECT, -} - - -class SonosSubscriptionsFailed(PlayerCommandFailed): - """Subscription creation failed.""" - - -class SonosPlayer: - """Wrapper around Sonos/SoCo with some additional attributes.""" - - def __init__( - self, - sonos_prov: SonosPlayerProvider, - soco: SoCo, - mass_player: Player, - ) -> None: - """Initialize SonosPlayer instance.""" - self.sonos_prov = sonos_prov - self.mass = sonos_prov.mass - self.player_id = soco.uid - self.soco = soco - self.logger = sonos_prov.logger - self.household_id: str = soco.household_id - self.subscriptions: list[SubscriptionBase] = [] - self.mass_player: Player = mass_player - self.available: bool = True - # cached attributes - self.crossfade: bool = False - self.play_mode: str | None = None - self.playback_status: str | None = None - self.channel: str | None = None - self.duration: float | None = None - self.image_url: str | None = None - self.source_name: str | None = None - self.title: str | None = None - self.uri: str | None = None - self.position: int | None = None - self.position_updated_at: datetime.datetime | None = None - self.loudness: bool = False - self.bass: int = 0 - self.treble: int = 0 - # Subscriptions and events - self._subscriptions: list[SubscriptionBase] = [] - self._subscription_lock: asyncio.Lock | None = None - self._last_activity: float = NEVER_TIME - self._resub_cooldown_expires_at: float | None = None - # Grouping - self.sync_coordinator: SonosPlayer | None = None - self.group_members: list[SonosPlayer] = [self] - self.group_members_ids: list[str] = [] - self._group_members_missing: set[str] = set() - - def __hash__(self) -> int: - """Return a hash of self.""" - return hash(self.player_id) - - @property - def zone_name(self) -> str: - """Return zone name.""" - if self.mass_player: - return self.mass_player.display_name - return self.soco.speaker_info["zone_name"] - - @property - def subscription_address(self) -> str: - """Return the current subscription callback address.""" - assert len(self._subscriptions) > 0 - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - - @property - def missing_subscriptions(self) -> set[str]: - """Return a list of missing service subscriptions.""" - subscribed_services = {sub.service.service_type for sub in self._subscriptions} - return SUBSCRIPTION_SERVICES - subscribed_services - - @property - def should_poll(self) -> bool: - """Return if this player should be polled/pinged.""" - if not self.available: - return True - return (time.monotonic() - self._last_activity) > self.mass_player.poll_interval - - def setup(self) -> None: - """Run initial setup of the speaker (NOT async friendly).""" - if self.soco.is_coordinator: - self.crossfade = self.soco.cross_fade - self.mass_player.volume_level = self.soco.volume - self.mass_player.volume_muted = self.soco.mute - self.loudness = self.soco.loudness - self.bass = self.soco.bass - self.treble = self.soco.treble - self.update_groups() - if not self.sync_coordinator: - self.poll_media() - - asyncio.run_coroutine_threadsafe(self.subscribe(), self.mass.loop) - - async def offline(self) -> None: - """Handle removal of speaker when unavailable.""" - if not self.available: - return - - if self._resub_cooldown_expires_at is None and not self.mass.closing: - self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS - self.logger.debug("Starting resubscription cooldown for %s", self.zone_name) - - self.available = False - self.mass_player.available = False - self.mass.players.update(self.player_id) - self._share_link_plugin = None - - await self.unsubscribe() - - def log_subscription_result(self, result: Any, event: str, level: int = logging.DEBUG) -> None: - """Log a message if a subscription action (create/renew/stop) results in an exception.""" - if not isinstance(result, Exception): - return - - if isinstance(result, asyncio.exceptions.TimeoutError): - message = "Request timed out" - exc_info = None - else: - message = str(result) - exc_info = result if not str(result) else None - - self.logger.log( - level, - "%s failed for %s: %s", - event, - self.zone_name, - message, - exc_info=exc_info if self.logger.isEnabledFor(10) else None, - ) - - async def subscribe(self) -> None: - """Initiate event subscriptions under an async lock.""" - if not self._subscription_lock: - self._subscription_lock = asyncio.Lock() - - async with self._subscription_lock: - try: - # Create event subscriptions. - subscriptions = [ - self._subscribe_target(getattr(self.soco, service), self._handle_event) - for service in self.missing_subscriptions - ] - if not subscriptions: - return - self.logger.log(VERBOSE_LOG_LEVEL, "Creating subscriptions for %s", self.zone_name) - results = await asyncio.gather(*subscriptions, return_exceptions=True) - for result in results: - self.log_subscription_result(result, "Creating subscription", logging.WARNING) - if any(isinstance(result, Exception) for result in results): - raise SonosSubscriptionsFailed - except SonosSubscriptionsFailed: - self.logger.warning("Creating subscriptions failed for %s", self.zone_name) - assert self._subscription_lock is not None - async with self._subscription_lock: - await self.offline() - - async def unsubscribe(self) -> None: - """Cancel all subscriptions.""" - if not self._subscriptions: - return - self.logger.log(VERBOSE_LOG_LEVEL, "Unsubscribing from events for %s", self.zone_name) - results = await asyncio.gather( - *(subscription.unsubscribe() for subscription in self._subscriptions), - return_exceptions=True, - ) - for result in results: - self.log_subscription_result(result, "Unsubscribe") - self._subscriptions = [] - - async def check_poll(self) -> bool: - """Validate availability of the speaker based on recent activity.""" - if not self.should_poll: - return False - self.logger.log(VERBOSE_LOG_LEVEL, "Polling player for availability...") - try: - await asyncio.to_thread(self.ping) - self._speaker_activity("ping") - except SonosUpdateError: - if not self.available: - return False # already offline - self.logger.warning( - "No recent activity and cannot reach %s, marking unavailable", - self.zone_name, - ) - await self.offline() - return True - - def update_ip(self, ip_address: str) -> None: - """Handle updated IP of a Sonos player (NOT async friendly).""" - if self.available: - return - self.logger.debug( - "Player IP-address changed from %s to %s", self.soco.ip_address, ip_address - ) - try: - self.ping() - except SonosUpdateError: - return - self.soco.ip_address = ip_address - self.setup() - self.mass_player.device_info = DeviceInfo( - model=self.mass_player.device_info.model, - address=ip_address, - manufacturer=self.mass_player.device_info.manufacturer, - ) - self.update_player() - - @soco_error() - def ping(self) -> None: - """Test device availability. Failure will raise SonosUpdateError.""" - self.soco.renderingControl.GetVolume([("InstanceID", 0), ("Channel", "Master")], timeout=1) - - async def join( - self, - members: list[SonosPlayer], - ) -> None: - """Sync given players/speakers with this player.""" - async with self.sonos_prov.topology_condition: - group: list[SonosPlayer] = await self.mass.create_task(self._join, members) - await self.wait_for_groups([group]) - - async def unjoin(self) -> None: - """Unjoin player from all/any groups.""" - async with self.sonos_prov.topology_condition: - await self.mass.create_task(self._unjoin) - await self.wait_for_groups([[self]]) - - def update_player(self, signal_update: bool = True) -> None: - """Update Sonos Player.""" - self._update_attributes() - if signal_update: - # send update to the player manager right away only if we are triggered from an event - # when we're just updating from a manual poll, the player manager - # will detect changes to the player object itself - self.mass.loop.call_soon_threadsafe(self.sonos_prov.mass.players.update, self.player_id) - - async def poll_speaker(self) -> None: - """Poll the speaker for updates.""" - - def _poll(): - """Poll the speaker for updates (NOT async friendly).""" - self.update_groups() - self.poll_media() - self.mass_player.volume_level = self.soco.volume - self.mass_player.volume_muted = self.soco.mute - - await asyncio.to_thread(_poll) - - @soco_error() - def poll_media(self) -> None: - """Poll information about currently playing media.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info["current_transport_state"] - - if new_status == SONOS_STATE_TRANSITIONING: - return - - update_position = new_status != self.playback_status - self.playback_status = new_status - self.play_mode = self.soco.play_mode - self._set_basic_track_info(update_position=update_position) - self.update_player() - - async def _subscribe_target(self, target: SubscriptionBase, sub_callback: Callable) -> None: - """Create a Sonos subscription for given target.""" - subscription = await target.subscribe( - auto_renew=True, requested_timeout=SUBSCRIPTION_TIMEOUT - ) - - def on_renew_failed(exception: Exception) -> None: - """Handle a failed subscription renewal callback.""" - self.mass.create_task(self._renew_failed(exception)) - - subscription.callback = sub_callback - subscription.auto_renew_fail = on_renew_failed - self._subscriptions.append(subscription) - - async def _renew_failed(self, exception: Exception) -> None: - """Mark the speaker as offline after a subscription renewal failure. - - This is to reset the state to allow a future clean subscription attempt. - """ - if not self.available: - return - - self.log_subscription_result(exception, "Subscription renewal", logging.WARNING) - await self.offline() - - def _handle_event(self, event: SonosEvent) -> None: - """Handle SonosEvent callback.""" - service_type: str = event.service.service_type - self._speaker_activity(f"{service_type} subscription") - - if service_type == "DeviceProperties": - self.update_player() - return - if service_type == "AVTransport": - self._handle_avtransport_event(event) - return - if service_type == "RenderingControl": - self._handle_rendering_control_event(event) - return - if service_type == "ZoneGroupTopology": - self._handle_zone_group_topology_event(event) - return - - def _handle_avtransport_event(self, event: SonosEvent) -> None: - """Update information about currently playing media from an event.""" - # NOTE: The new coordinator can be provided in a media update event but - # before the ZoneGroupState updates. If this happens the playback - # state will be incorrect and should be ignored. Switching to the - # new coordinator will use its media. The regrouping process will - # be completed during the next ZoneGroupState update. - av_transport_uri = event.variables.get("av_transport_uri", "") - current_track_uri = event.variables.get("current_track_uri", "") - if av_transport_uri == current_track_uri and av_transport_uri.startswith("x-rincon:"): - new_coordinator_uid = av_transport_uri.split(":")[-1] - if new_coordinator_speaker := self.sonos_prov.sonosplayers.get(new_coordinator_uid): - self.logger.log( - 5, - "Media update coordinator (%s) received for %s", - new_coordinator_speaker.zone_name, - self.zone_name, - ) - self.sync_coordinator = new_coordinator_speaker - else: - self.logger.debug( - "Media update coordinator (%s) for %s not yet available", - new_coordinator_uid, - self.zone_name, - ) - return - - if crossfade := event.variables.get("current_crossfade_mode"): - self.crossfade = bool(int(crossfade)) - - # Missing transport_state indicates a transient error - if (new_status := event.variables.get("transport_state")) is None: - return - - # Ignore transitions, we should get the target state soon - if new_status == SONOS_STATE_TRANSITIONING: - return - - evars = event.variables - new_status = evars["transport_state"] - state_changed = new_status != self.playback_status - - self.play_mode = evars["current_play_mode"] - self.playback_status = new_status - - track_uri = evars["enqueued_transport_uri"] or evars["current_track_uri"] - audio_source = self.soco.music_source_from_uri(track_uri) - - self._set_basic_track_info(update_position=state_changed) - - if (ct_md := evars["current_track_meta_data"]) and not self.image_url: - if album_art_uri := getattr(ct_md, "album_art_uri", None): - # TODO: handle library mess here - self.image_url = album_art_uri - - et_uri_md = evars["enqueued_transport_uri_meta_data"] - if isinstance(et_uri_md, DidlPlaylistContainer): - self.playlist_name = et_uri_md.title - - if queue_size := evars.get("number_of_tracks", 0): - self.queue_size = int(queue_size) - - if audio_source == MUSIC_SRC_RADIO: - if et_uri_md: - self.channel = et_uri_md.title - - # Extra guards for S1 compatibility - if ct_md and hasattr(ct_md, "radio_show") and ct_md.radio_show: - radio_show = ct_md.radio_show.split(",")[0] - self.channel = " • ".join(filter(None, [self.channel, radio_show])) - - if isinstance(et_uri_md, DidlAudioBroadcast): - self.title = self.title or self.channel - - self.update_player() - - def _handle_rendering_control_event(self, event: SonosEvent) -> None: - """Update information about currently volume settings.""" - variables = event.variables - - if "volume" in variables: - volume = variables["volume"] - self.mass_player.volume_level = int(volume["Master"]) - - if mute := variables.get("mute"): - self.mass_player.volume_muted = mute["Master"] == "1" - - self.update_player() - - def _handle_zone_group_topology_event(self, event: SonosEvent) -> None: - """Handle callback for topology change event.""" - if "zone_player_uui_ds_in_group" not in event.variables: - return - asyncio.run_coroutine_threadsafe(self.create_update_groups_coro(event), self.mass.loop) - - async def _rebooted(self) -> None: - """Handle a detected speaker reboot.""" - self.logger.debug("%s rebooted, reconnecting", self.zone_name) - await self.offline() - self._speaker_activity("reboot") - - def update_groups(self) -> None: - """Update group topology when polling.""" - asyncio.run_coroutine_threadsafe(self.create_update_groups_coro(), self.mass.loop) - - def update_group_for_uid(self, uid: str) -> None: - """Update group topology if uid is missing.""" - if uid not in self._group_members_missing: - return - missing_zone = self.sonos_prov.sonosplayers[uid].zone_name - self.logger.debug("%s was missing, adding to %s group", missing_zone, self.zone_name) - self.update_groups() - - def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: - """Handle callback for topology change event.""" - - def _get_soco_group() -> list[str]: - """Ask SoCo cache for existing topology.""" - coordinator_uid = self.soco.uid - joined_uids = [] - with contextlib.suppress(OSError, SoCoException): - if self.soco.group and self.soco.group.coordinator: - coordinator_uid = self.soco.group.coordinator.uid - joined_uids = [ - p.uid - for p in self.soco.group.members - if p.uid != coordinator_uid and p.is_visible - ] - - return [coordinator_uid, *joined_uids] - - async def _extract_group(event: SonosEvent | None) -> list[str]: - """Extract group layout from a topology event.""" - group = event and event.zone_player_uui_ds_in_group - if group: - assert isinstance(group, str) - return group.split(",") - return await self.mass.create_task(_get_soco_group) - - def _regroup(group: list[str]) -> None: - """Rebuild internal group layout (async safe).""" - if group == [self.soco.uid] and self.group_members == [self] and self.group_members_ids: - # Skip updating existing single speakers in polling mode - return - - group_members = [] - group_members_ids = [] - - for uid in group: - speaker = self.sonos_prov.sonosplayers.get(uid) - if speaker: - self._group_members_missing.discard(uid) - group_members.append(speaker) - group_members_ids.append(uid) - else: - self._group_members_missing.add(uid) - self.logger.debug( - "%s group member unavailable (%s), will try again", - self.zone_name, - uid, - ) - return - - if self.group_members_ids == group_members_ids: - # Useful in polling mode for speakers with stereo pairs or surrounds - # as those "invisible" speakers will bypass the single speaker check - return - - self.sync_coordinator = None - self.group_members = group_members - self.group_members_ids = group_members_ids - self.mass.loop.call_soon_threadsafe(self.mass.players.update, self.player_id) - - for joined_uid in group[1:]: - joined_speaker: SonosPlayer = self.sonos_prov.sonosplayers.get(joined_uid) - if joined_speaker: - joined_speaker.sync_coordinator = self - joined_speaker.group_members = group_members - joined_speaker.group_members_ids = group_members_ids - joined_speaker.update_player() - - self.logger.debug("Regrouped %s: %s", self.zone_name, self.group_members_ids) - self.update_player() - - async def _handle_group_event(event: SonosEvent | None) -> None: - """Get async lock and handle event.""" - async with self.sonos_prov.topology_condition: - group = await _extract_group(event) - if self.soco.uid == group[0]: - _regroup(group) - self.sonos_prov.topology_condition.notify_all() - - return _handle_group_event(event) - - async def wait_for_groups(self, groups: list[list[SonosPlayer]]) -> None: - """Wait until all groups are present, or timeout.""" - - def _test_groups(groups: list[list[SonosPlayer]]) -> bool: - """Return whether all groups exist now.""" - for group in groups: - coordinator = group[0] - - # Test that coordinator is coordinating - current_group = coordinator.group_members - if coordinator != current_group[0]: - return False - - # Test that joined members match - if set(group[1:]) != set(current_group[1:]): - return False - - return True - - try: - async with asyncio.timeout(5): - while not _test_groups(groups): - await self.sonos_prov.topology_condition.wait() - except TimeoutError: - self.logger.warning("Timeout waiting for target groups %s", groups) - - any_speaker = next(iter(self.sonos_prov.sonosplayers.values())) - any_speaker.soco.zone_group_state.clear_cache() - - def _update_attributes(self) -> None: - """Update attributes of the MA Player from SoCo state.""" - # generic attributes (player_info) - self.mass_player.available = self.available - - if not self.available: - self.mass_player.powered = False - self.mass_player.state = PlayerState.IDLE - self.mass_player.synced_to = None - self.mass_player.group_childs = set() - return - - # transport info (playback state) - self.mass_player.state = current_state = _convert_state(self.playback_status) - - # power 'on' player if we detect its playing - if not self.mass_player.powered and ( - current_state == PlayerState.PLAYING - or ( - self.sync_coordinator - and self.sync_coordinator.mass_player.state == PlayerState.PLAYING - ) - ): - self.mass_player.powered = True - - # media info (track info) - self.mass_player.current_item_id = self.uri - if self.uri and self.mass.streams.base_url in self.uri and self.player_id in self.uri: - self.mass_player.active_source = self.player_id - else: - self.mass_player.active_source = self.source_name - if self.position is not None and self.position_updated_at is not None: - self.mass_player.elapsed_time = self.position - self.mass_player.elapsed_time_last_updated = self.position_updated_at.timestamp() - - # zone topology (syncing/grouping) details - if self.sync_coordinator: - # player is synced to another player - self.mass_player.synced_to = self.sync_coordinator.player_id - self.mass_player.group_childs = set() - self.mass_player.active_source = self.sync_coordinator.mass_player.active_source - elif len(self.group_members_ids) > 1: - # this player is the sync leader in a group - self.mass_player.synced_to = None - self.mass_player.group_childs = set(self.group_members_ids) - else: - # standalone player, not synced - self.mass_player.synced_to = None - self.mass_player.group_childs = set() - - def _set_basic_track_info(self, update_position: bool = False) -> None: - """Query the speaker to update media metadata and position info.""" - self.channel = None - self.duration = None - self.image_url = None - self.source_name = None - self.title = None - self.uri = None - - try: - track_info = self._poll_track_info() - except SonosUpdateError as err: - self.logger.warning("Fetching track info failed: %s", err) - return - if not track_info["uri"]: - return - self.uri = track_info["uri"] - - audio_source = self.soco.music_source_from_uri(self.uri) - if source := SOURCE_MAPPING.get(audio_source): - self.source_name = source - if audio_source in LINEIN_SOURCES: - self.position = None - self.position_updated_at = None - self.title = source - return - - self.artist = track_info.get("artist") - self.album_name = track_info.get("album") - self.title = track_info.get("title") - self.image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position", -1)) - if playlist_position > 0: - self.queue_position = playlist_position - - self._update_media_position(track_info, force_update=update_position) - - def _update_media_position( - self, position_info: dict[str, int], force_update: bool = False - ) -> None: - """Update state when playing music tracks.""" - duration = position_info.get(DURATION_SECONDS) - current_position = position_info.get(POSITION_SECONDS) - - if not (duration or current_position): - self.position = None - self.position_updated_at = None - return - - should_update = force_update - self.duration = duration - - # player started reporting position? - if current_position is not None and self.position is None: - should_update = True - - # position jumped? - if current_position is not None and self.position is not None: - if self.playback_status == SONOS_STATE_PLAYING: - assert self.position_updated_at is not None - time_delta = utc() - self.position_updated_at - time_diff = time_delta.total_seconds() - else: - time_diff = 0 - - calculated_position = self.position + time_diff - - if abs(calculated_position - current_position) > 1.5: - should_update = True - - if current_position is None: - self.position = None - self.position_updated_at = None - elif should_update: - self.position = current_position - self.position_updated_at = utc() - - def _speaker_activity(self, source: str) -> None: - """Track the last activity on this speaker, set availability and resubscribe.""" - if self._resub_cooldown_expires_at: - if time.monotonic() < self._resub_cooldown_expires_at: - self.logger.debug( - "Activity on %s from %s while in cooldown, ignoring", - self.zone_name, - source, - ) - return - self._resub_cooldown_expires_at = None - - self.logger.log(VERBOSE_LOG_LEVEL, "Activity on %s from %s", self.zone_name, source) - self._last_activity = time.monotonic() - was_available = self.available - self.available = True - if not was_available: - self.update_player() - self.mass.loop.call_soon_threadsafe(self.mass.create_task, self.subscribe()) - - @soco_error() - def _join(self, members: list[SonosPlayer]) -> list[SonosPlayer]: - if self.sync_coordinator: - self.unjoin() - group = [self] - else: - group = self.group_members.copy() - - for player in members: - if player.soco.uid != self.soco.uid and player not in group: - player.soco.join(self.soco) - player.sync_coordinator = self - group.append(player) - - return group - - @soco_error() - def _unjoin(self) -> None: - if self.group_members == [self]: - return - self.soco.unjoin() - self.sync_coordinator = None - - @soco_error() - def _poll_track_info(self) -> dict[str, Any]: - """Poll the speaker for current track info. - - Add converted position values (NOT async fiendly). - """ - track_info: dict[str, Any] = self.soco.get_current_track_info() - track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) - track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) - return track_info - - -def _convert_state(sonos_state: str) -> PlayerState: - """Convert Sonos state to PlayerState.""" - if sonos_state == "PLAYING": - return PlayerState.PLAYING - if sonos_state == "TRANSITIONING": - return PlayerState.PLAYING - if sonos_state == "PAUSED_PLAYBACK": - return PlayerState.PAUSED - return PlayerState.IDLE - - -def _timespan_secs(timespan): - """Parse a time-span into number of seconds.""" - if timespan in ("", "NOT_IMPLEMENTED", None): - return None - return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/server/providers/soundcloud/__init__.py deleted file mode 100644 index 3b6bae56..00000000 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ /dev/null @@ -1,442 +0,0 @@ -"""Soundcloud support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import time -from typing import TYPE_CHECKING - -from soundcloudpy import SoundcloudAsyncAPI - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import InvalidDataError, LoginFailed -from music_assistant.common.models.media_items import ( - Artist, - AudioFormat, - ContentType, - ImageType, - MediaItemImage, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.models.music_provider import MusicProvider - -CONF_CLIENT_ID = "client_id" -CONF_AUTHORIZATION = "authorization" - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, -) - - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - if not config.get_value(CONF_CLIENT_ID) or not config.get_value(CONF_AUTHORIZATION): - msg = "Invalid login credentials" - raise LoginFailed(msg) - return SoundcloudMusicProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_CLIENT_ID, - type=ConfigEntryType.SECURE_STRING, - label="Client ID", - required=True, - ), - ConfigEntry( - key=CONF_AUTHORIZATION, - type=ConfigEntryType.SECURE_STRING, - label="Authorization", - required=True, - ), - ) - - -class SoundcloudMusicProvider(MusicProvider): - """Provider for Soundcloud.""" - - _headers = None - _context = None - _cookies = None - _signature_timestamp = 0 - _cipher = None - _user_id = None - _soundcloud = None - _me = None - - async def handle_async_init(self) -> None: - """Set up the Soundcloud provider.""" - client_id = self.config.get_value(CONF_CLIENT_ID) - auth_token = self.config.get_value(CONF_AUTHORIZATION) - self._soundcloud = SoundcloudAsyncAPI(auth_token, client_id, self.mass.http_session) - await self._soundcloud.login() - self._me = await self._soundcloud.get_account_details() - self._user_id = self._me["id"] - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - @classmethod - async def _run_async(cls, call: Callable, *args, **kwargs): # noqa: ANN206 - return await asyncio.to_thread(call, *args, **kwargs) - - async def search( - self, search_query: str, media_types=list[MediaType], limit: int = 10 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - result = SearchResults() - searchtypes = [] - if MediaType.ARTIST in media_types: - searchtypes.append("artist") - if MediaType.TRACK in media_types: - searchtypes.append("track") - if MediaType.PLAYLIST in media_types: - searchtypes.append("playlist") - - media_types = [ - x for x in media_types if x in (MediaType.ARTIST, MediaType.TRACK, MediaType.PLAYLIST) - ] - if not media_types: - return result - - searchresult = await self._soundcloud.search(search_query, limit) - - for item in searchresult["collection"]: - media_type = item["kind"] - if media_type == "user" and MediaType.ARTIST in media_types: - result.artists.append(await self._parse_artist(item)) - elif media_type == "track" and MediaType.TRACK in media_types: - if item.get("duration") == item.get("full_duration"): - # skip if it's a preview track (e.g. in case of free accounts) - result.tracks.append(await self._parse_track(item)) - elif media_type == "playlist" and MediaType.PLAYLIST in media_types: - result.playlists.append(await self._parse_playlist(item)) - - return result - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Soundcloud.""" - time_start = time.time() - - following = await self._soundcloud.get_following(self._user_id) - self.logger.debug( - "Processing Soundcloud library artists took %s seconds", - round(time.time() - time_start, 2), - ) - for artist in following["collection"]: - try: - yield await self._parse_artist(artist) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse artist failed: %s", artist, exc_info=error) - continue - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from Soundcloud.""" - time_start = time.time() - async for item in self._soundcloud.get_account_playlists(): - try: - raw_playlist = item["playlist"] - except KeyError: - self.logger.debug( - "Unexpected Soundcloud API response when parsing playlists: %s", - item, - ) - continue - - try: - playlist = await self._soundcloud.get_playlist_details( - playlist_id=raw_playlist["id"], - ) - - yield await self._parse_playlist(playlist) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug( - "Failed to obtain Soundcloud playlist details: %s", - raw_playlist, - exc_info=error, - ) - continue - - self.logger.debug( - "Processing Soundcloud library playlists took %s seconds", - round(time.time() - time_start, 2), - ) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Soundcloud.""" - time_start = time.time() - async for item in self._soundcloud.get_tracks_liked(): - track = await self._soundcloud.get_track_details(item) - try: - yield await self._parse_track(track[0]) - except IndexError: - continue - except (KeyError, TypeError, InvalidDataError) as error: - self.logger.debug("Parse track failed: %s", track, exc_info=error) - continue - - self.logger.debug( - "Processing Soundcloud library tracks took %s seconds", - round(time.time() - time_start, 2), - ) - - async def get_artist(self, prov_artist_id) -> Artist: - """Get full artist details by id.""" - artist_obj = await self._soundcloud.get_user_details(user_id=prov_artist_id) - try: - artist = await self._parse_artist(artist_obj=artist_obj) if artist_obj else None - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse artist failed: %s", artist_obj, exc_info=error) - return artist - - async def get_track(self, prov_track_id) -> Track: - """Get full track details by id.""" - track_obj = await self._soundcloud.get_track_details(track_id=prov_track_id) - try: - track = await self._parse_track(track_obj[0]) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse track failed: %s", track_obj, exc_info=error) - return track - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Get full playlist details by id.""" - playlist_obj = await self._soundcloud.get_playlist_details(playlist_id=prov_playlist_id) - try: - playlist = await self._parse_playlist(playlist_obj) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse playlist failed: %s", playlist_obj, exc_info=error) - return playlist - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - if page > 0: - # TODO: soundcloud doesn't seem to support paging for playlist tracks ?! - return result - playlist_obj = await self._soundcloud.get_playlist_details(playlist_id=prov_playlist_id) - if "tracks" not in playlist_obj: - return result - for index, item in enumerate(playlist_obj["tracks"], 1): - # TODO: is it really needed to grab the entire track with an api call ? - song = await self._soundcloud.get_track_details(item["id"]) - try: - if track := await self._parse_track(song[0], index): - result.append(track) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse track failed: %s", song, exc_info=error) - continue - return result - - async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: - """Get a list of 25 most popular tracks for the given artist.""" - tracks_obj = await self._soundcloud.get_popular_tracks_user( - user_id=prov_artist_id, limit=25 - ) - tracks = [] - for item in tracks_obj["collection"]: - song = await self._soundcloud.get_track_details(item["id"]) - try: - track = await self._parse_track(song[0]) - tracks.append(track) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse track failed: %s", song, exc_info=error) - continue - return tracks - - async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - tracks_obj = await self._soundcloud.get_recommended(track_id=prov_track_id, limit=limit) - tracks = [] - for item in tracks_obj["collection"]: - song = await self._soundcloud.get_track_details(item["id"]) - try: - track = await self._parse_track(song[0]) - tracks.append(track) - except (KeyError, TypeError, InvalidDataError, IndexError) as error: - self.logger.debug("Parse track failed: %s", song, exc_info=error) - continue - - return tracks - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - url: str = await self._soundcloud.get_stream_url(track_id=item_id) - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - # let ffmpeg work out the details itself as - # soundcloud uses a mix of different content types and streaming methods - audio_format=AudioFormat( - content_type=ContentType.UNKNOWN, - ), - stream_type=StreamType.HLS - if url.startswith("https://cf-hls-media.sndcdn.com") - else StreamType.HTTP, - path=url, - ) - - async def _parse_artist(self, artist_obj: dict) -> Artist: - """Parse a Soundcloud user response to Artist model object.""" - artist_id = None - permalink = artist_obj["permalink"] - if artist_obj.get("id"): - artist_id = artist_obj["id"] - if not artist_id: - msg = "Artist does not have a valid ID" - raise InvalidDataError(msg) - artist_id = str(artist_id) - artist = Artist( - item_id=artist_id, - name=artist_obj["username"], - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f"https://soundcloud.com/{permalink}", - ) - }, - ) - if artist_obj.get("description"): - artist.metadata.description = artist_obj["description"] - if artist_obj.get("avatar_url"): - img_url = artist_obj["avatar_url"] - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img_url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - return artist - - async def _parse_playlist(self, playlist_obj: dict) -> Playlist: - """Parse a Soundcloud Playlist response to a Playlist object.""" - playlist_id = str(playlist_obj["id"]) - playlist = Playlist( - item_id=playlist_id, - provider=self.domain, - name=playlist_obj["title"], - provider_mappings={ - ProviderMapping( - item_id=playlist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - playlist.is_editable = False - if playlist_obj.get("description"): - playlist.metadata.description = playlist_obj["description"] - if playlist_obj.get("artwork_url"): - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=self._transform_artwork_url(playlist_obj["artwork_url"]), - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if playlist_obj.get("genre"): - playlist.metadata.genres = playlist_obj["genre"] - if playlist_obj.get("tag_list"): - playlist.metadata.style = playlist_obj["tag_list"] - return playlist - - async def _parse_track(self, track_obj: dict, playlist_position: int = 0) -> Track: - """Parse a Soundcloud Track response to a Track model object.""" - name, version = parse_title_and_version(track_obj["title"]) - track_id = str(track_obj["id"]) - track = Track( - item_id=track_id, - provider=self.domain, - name=name, - version=version, - duration=track_obj["duration"] / 1000, - provider_mappings={ - ProviderMapping( - item_id=track_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.MP3, - ), - url=track_obj["permalink_url"], - ) - }, - position=playlist_position, - ) - user_id = track_obj["user"]["id"] - user = await self._soundcloud.get_user_details(user_id) - artist = await self._parse_artist(user) - if artist and artist.item_id not in {x.item_id for x in track.artists}: - track.artists.append(artist) - - if track_obj.get("artwork_url"): - track.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=self._transform_artwork_url(track_obj["artwork_url"]), - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - if track_obj.get("description"): - track.metadata.description = track_obj["description"] - if track_obj.get("genre"): - track.metadata.genres = track_obj["genre"] - if track_obj.get("tag_list"): - track.metadata.style = track_obj["tag_list"] - return track - - def _transform_artwork_url(self, artwork_url: str) -> str: - """Patch artwork URL to a high quality thumbnail.""" - # This is undocumented in their API docs, but was previously - return artwork_url.replace("large", "t500x500") diff --git a/music_assistant/server/providers/soundcloud/icon.svg b/music_assistant/server/providers/soundcloud/icon.svg deleted file mode 100644 index 026d9566..00000000 --- a/music_assistant/server/providers/soundcloud/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/music_assistant/server/providers/soundcloud/manifest.json b/music_assistant/server/providers/soundcloud/manifest.json deleted file mode 100644 index c7e67910..00000000 --- a/music_assistant/server/providers/soundcloud/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "soundcloud", - "name": "Soundcloud", - "description": "Support for the Soundcloud streaming provider in Music Assistant.", - "codeowners": ["@domanchi", "@gieljnssns"], - "requirements": ["soundcloudpy==0.1.0"], - "documentation": "https://music-assistant.io/music-providers/soundcloud/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py deleted file mode 100644 index a3128e56..00000000 --- a/music_assistant/server/providers/spotify/__init__.py +++ /dev/null @@ -1,1024 +0,0 @@ -"""Spotify musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import contextlib -import os -import platform -import time -from typing import TYPE_CHECKING, Any, cast -from urllib.parse import urlencode - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - AudioError, - LoginFailed, - MediaNotFoundError, - ResourceTemporarilyUnavailable, - SetupFailedError, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - AudioFormat, - ContentType, - ImageType, - MediaItemImage, - MediaItemType, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.audio import get_chunksize -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.helpers.util import lock -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -CONF_CLIENT_ID = "client_id" -CONF_ACTION_AUTH = "auth" -CONF_REFRESH_TOKEN = "refresh_token" -CONF_ACTION_CLEAR_AUTH = "clear_auth" -SCOPE = [ - "playlist-read", - "playlist-read-private", - "playlist-read-collaborative", - "playlist-modify-public", - "playlist-modify-private", - "user-follow-modify", - "user-follow-read", - "user-library-read", - "user-library-modify", - "user-read-private", - "user-read-email", - "user-top-read", - "app-remote-control", - "streaming", - "user-read-playback-state", - "user-modify-playback-state", - "user-read-currently-playing", - "user-modify-private", - "user-modify", - "user-read-playback-position", - "user-read-recently-played", -] - -CALLBACK_REDIRECT_URL = "https://music-assistant.io/callback" - -CACHE_DIR = "/tmp/spotify_cache" # noqa: S108 -LIKED_SONGS_FAKE_PLAYLIST_ID_PREFIX = "liked_songs" -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - if config.get_value(CONF_REFRESH_TOKEN) in (None, ""): - msg = "Re-Authentication required" - raise SetupFailedError(msg) - return SpotifyProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - - if action == CONF_ACTION_AUTH: - # spotify PKCE auth flow - # https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow - import pkce - - code_verifier, code_challenge = pkce.generate_pkce_pair() - async with AuthenticationHelper(mass, cast(str, values["session_id"])) as auth_helper: - params = { - "response_type": "code", - "client_id": values.get(CONF_CLIENT_ID) or app_var(2), - "scope": " ".join(SCOPE), - "code_challenge_method": "S256", - "code_challenge": code_challenge, - "redirect_uri": CALLBACK_REDIRECT_URL, - "state": auth_helper.callback_url, - } - query_string = urlencode(params) - url = f"https://accounts.spotify.com/authorize?{query_string}" - result = await auth_helper.authenticate(url) - authorization_code = result["code"] - # now get the access token - params = { - "grant_type": "authorization_code", - "code": authorization_code, - "redirect_uri": CALLBACK_REDIRECT_URL, - "client_id": values.get(CONF_CLIENT_ID) or app_var(2), - "code_verifier": code_verifier, - } - async with mass.http_session.post( - "https://accounts.spotify.com/api/token", data=params - ) as response: - result = await response.json() - values[CONF_REFRESH_TOKEN] = result["refresh_token"] - - # handle action clear authentication - if action == CONF_ACTION_CLEAR_AUTH: - assert values - values[CONF_REFRESH_TOKEN] = None - - auth_required = values.get(CONF_REFRESH_TOKEN) in (None, "") - - if auth_required: - values[CONF_CLIENT_ID] = None - label_text = ( - "You need to authenticate to Spotify. Click the authenticate button below " - "to start the authentication process which will open in a new (popup) window, " - "so make sure to disable any popup blockers.\n\n" - "Also make sure to perform this action from your local network" - ) - elif action == CONF_ACTION_AUTH: - label_text = "Authenticated to Spotify. Press save to complete setup." - else: - label_text = "Authenticated to Spotify. No further action required." - - return ( - ConfigEntry( - key="label_text", - type=ConfigEntryType.LABEL, - label=label_text, - ), - ConfigEntry( - key=CONF_REFRESH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label=CONF_REFRESH_TOKEN, - hidden=True, - required=True, - value=values.get(CONF_REFRESH_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_CLIENT_ID, - type=ConfigEntryType.SECURE_STRING, - label="Client ID (optional)", - description="By default, a generic client ID is used which is heavy rate limited. " - "It is advised that you create your own Spotify Developer account and use " - "that client ID here to speedup performance. \n\n" - f"Use {CALLBACK_REDIRECT_URL} as callback URL.", - required=False, - value=values.get(CONF_CLIENT_ID) if values else None, - hidden=not auth_required, - ), - ConfigEntry( - key=CONF_ACTION_AUTH, - type=ConfigEntryType.ACTION, - label="Authenticate with Spotify", - description="This button will redirect you to Spotify to authenticate.", - action=CONF_ACTION_AUTH, - hidden=not auth_required, - ), - ConfigEntry( - key=CONF_ACTION_CLEAR_AUTH, - type=ConfigEntryType.ACTION, - label="Clear authentication", - description="Clear the current authentication details.", - action=CONF_ACTION_CLEAR_AUTH, - action_label="Clear authentication", - required=False, - hidden=auth_required, - ), - ) - - -class SpotifyProvider(MusicProvider): - """Implementation of a Spotify MusicProvider.""" - - _auth_info: str | None = None - _sp_user: dict[str, Any] | None = None - _librespot_bin: str | None = None - throttler: ThrottlerManager - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.config_dir = os.path.join(self.mass.storage_path, self.instance_id) - self.throttler = ThrottlerManager(rate_limit=1, period=2) - if self.config.get_value(CONF_CLIENT_ID): - # loosen the throttler a bit when a custom client id is used - self.throttler.rate_limit = 45 - self.throttler.period = 30 - # check if we have a librespot binary for this arch - await self.get_librespot_binary() - # try login which will raise if it fails - await self.login() - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ProviderFeature.PLAYLIST_CREATE, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, - ) - - @property - def name(self) -> str: - """Return (custom) friendly name for this provider instance.""" - if self._sp_user: - postfix = self._sp_user["display_name"] - return f"{self.manifest.name}: {postfix}" - return super().name - - async def search( - self, search_query: str, media_types=list[MediaType], limit: int = 5 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - searchresult = SearchResults() - searchtypes = [] - if MediaType.ARTIST in media_types: - searchtypes.append("artist") - if MediaType.ALBUM in media_types: - searchtypes.append("album") - if MediaType.TRACK in media_types: - searchtypes.append("track") - if MediaType.PLAYLIST in media_types: - searchtypes.append("playlist") - if not searchtypes: - return searchresult - searchtype = ",".join(searchtypes) - search_query = search_query.replace("'", "") - api_result = await self._get_data("search", q=search_query, type=searchtype, limit=limit) - if "artists" in api_result: - searchresult.artists += [ - self._parse_artist(item) - for item in api_result["artists"]["items"] - if (item and item["id"] and item["name"]) - ] - if "albums" in api_result: - searchresult.albums += [ - self._parse_album(item) - for item in api_result["albums"]["items"] - if (item and item["id"]) - ] - if "tracks" in api_result: - searchresult.tracks += [ - self._parse_track(item) - for item in api_result["tracks"]["items"] - if (item and item["id"]) - ] - if "playlists" in api_result: - searchresult.playlists += [ - self._parse_playlist(item) - for item in api_result["playlists"]["items"] - if (item and item["id"]) - ] - return searchresult - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve library artists from spotify.""" - endpoint = "me/following" - while True: - spotify_artists = await self._get_data( - endpoint, - type="artist", - limit=50, - ) - for item in spotify_artists["artists"]["items"]: - if item and item["id"]: - yield self._parse_artist(item) - if spotify_artists["artists"]["next"]: - endpoint = spotify_artists["artists"]["next"] - endpoint = endpoint.replace("https://api.spotify.com/v1/", "") - else: - break - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve library albums from the provider.""" - for item in await self._get_all_items("me/albums"): - if item["album"] and item["album"]["id"]: - yield self._parse_album(item["album"]) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - for item in await self._get_all_items("me/tracks"): - if item and item["track"]["id"]: - yield self._parse_track(item["track"]) - - def _get_liked_songs_playlist_id(self) -> str: - return f"{LIKED_SONGS_FAKE_PLAYLIST_ID_PREFIX}-{self.instance_id}" - - async def _get_liked_songs_playlist(self) -> Playlist: - liked_songs = Playlist( - item_id=self._get_liked_songs_playlist_id(), - provider=self.domain, - name=f'Liked Songs {self._sp_user["display_name"]}', # TODO to be translated - owner=self._sp_user["display_name"], - provider_mappings={ - ProviderMapping( - item_id=self._get_liked_songs_playlist_id(), - provider_domain=self.domain, - provider_instance=self.instance_id, - url="https://open.spotify.com/collection/tracks", - ) - }, - ) - - liked_songs.is_editable = False # TODO Editing requires special endpoints - - liked_songs.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path="https://misc.scdn.co/liked-songs/liked-songs-64.png", - provider=self.domain, - remotely_accessible=True, - ) - ] - - liked_songs.cache_checksum = str(time.time()) - - return liked_songs - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve playlists from the provider.""" - yield await self._get_liked_songs_playlist() - for item in await self._get_all_items("me/playlists"): - if item and item["id"]: - yield self._parse_playlist(item) - - async def get_artist(self, prov_artist_id) -> Artist: - """Get full artist details by id.""" - artist_obj = await self._get_data(f"artists/{prov_artist_id}") - return self._parse_artist(artist_obj) - - async def get_album(self, prov_album_id) -> Album: - """Get full album details by id.""" - album_obj = await self._get_data(f"albums/{prov_album_id}") - return self._parse_album(album_obj) - - async def get_track(self, prov_track_id) -> Track: - """Get full track details by id.""" - track_obj = await self._get_data(f"tracks/{prov_track_id}") - return self._parse_track(track_obj) - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Get full playlist details by id.""" - if prov_playlist_id == self._get_liked_songs_playlist_id(): - return await self._get_liked_songs_playlist() - - playlist_obj = await self._get_data(f"playlists/{prov_playlist_id}") - return self._parse_playlist(playlist_obj) - - async def get_album_tracks(self, prov_album_id) -> list[Track]: - """Get all album tracks for given album id.""" - return [ - self._parse_track(item) - for item in await self._get_all_items(f"albums/{prov_album_id}/tracks") - if item["id"] - ] - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - result: list[Track] = [] - uri = ( - "me/tracks" - if prov_playlist_id == self._get_liked_songs_playlist_id() - else f"playlists/{prov_playlist_id}/tracks" - ) - page_size = 50 - offset = page * page_size - spotify_result = await self._get_data(uri, limit=page_size, offset=offset) - for index, item in enumerate(spotify_result["items"], 1): - if not (item and item["track"] and item["track"]["id"]): - continue - # use count as position - track = self._parse_track(item["track"]) - track.position = offset + index - result.append(track) - return result - - async def get_artist_albums(self, prov_artist_id) -> list[Album]: - """Get a list of all albums for the given artist.""" - return [ - self._parse_album(item) - for item in await self._get_all_items( - f"artists/{prov_artist_id}/albums?include_groups=album,single,compilation" - ) - if (item and item["id"]) - ] - - async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: - """Get a list of 10 most popular tracks for the given artist.""" - artist = await self.get_artist(prov_artist_id) - endpoint = f"artists/{prov_artist_id}/top-tracks" - items = await self._get_data(endpoint) - return [ - self._parse_track(item, artist=artist) - for item in items["tracks"] - if (item and item["id"]) - ] - - async def library_add(self, item: MediaItemType): - """Add item to library.""" - if item.media_type == MediaType.ARTIST: - await self._put_data("me/following", {"ids": [item.item_id]}, type="artist") - elif item.media_type == MediaType.ALBUM: - await self._put_data("me/albums", {"ids": [item.item_id]}) - elif item.media_type == MediaType.TRACK: - await self._put_data("me/tracks", {"ids": [item.item_id]}) - elif item.media_type == MediaType.PLAYLIST: - await self._put_data(f"playlists/{item.item_id}/followers", data={"public": False}) - return True - - async def library_remove(self, prov_item_id, media_type: MediaType): - """Remove item from library.""" - if media_type == MediaType.ARTIST: - await self._delete_data("me/following", {"ids": [prov_item_id]}, type="artist") - elif media_type == MediaType.ALBUM: - await self._delete_data("me/albums", {"ids": [prov_item_id]}) - elif media_type == MediaType.TRACK: - await self._delete_data("me/tracks", {"ids": [prov_item_id]}) - elif media_type == MediaType.PLAYLIST: - await self._delete_data(f"playlists/{prov_item_id}/followers") - return True - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]): - """Add track(s) to playlist.""" - track_uris = [f"spotify:track:{track_id}" for track_id in prov_track_ids] - data = {"uris": track_uris} - await self._post_data(f"playlists/{prov_playlist_id}/tracks", data=data) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - track_uris = [] - for pos in positions_to_remove: - uri = f"playlists/{prov_playlist_id}/tracks" - spotify_result = await self._get_data(uri, limit=1, offset=pos - 1) - for item in spotify_result["items"]: - if not (item and item["track"] and item["track"]["id"]): - continue - track_uris.append({"uri": f'spotify:track:{item["track"]["id"]}'}) - data = {"tracks": track_uris} - await self._delete_data(f"playlists/{prov_playlist_id}/tracks", data=data) - - async def create_playlist(self, name: str) -> Playlist: - """Create a new playlist on provider with given name.""" - data = {"name": name, "public": False} - new_playlist = await self._post_data(f"users/{self._sp_user['id']}/playlists", data=data) - self._fix_create_playlist_api_bug(new_playlist) - return self._parse_playlist(new_playlist) - - async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - endpoint = "recommendations" - items = await self._get_data(endpoint, seed_tracks=prov_track_id, limit=limit) - return [self._parse_track(item) for item in items["tracks"] if (item and item["id"])] - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - return StreamDetails( - item_id=item_id, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.OGG, - ), - stream_type=StreamType.CUSTOM, - ) - - async def get_audio_stream( - self, streamdetails: StreamDetails, seek_position: int = 0 - ) -> AsyncGenerator[bytes, None]: - """Return the audio stream for the provider item.""" - librespot = await self.get_librespot_binary() - spotify_uri = f"spotify://track:{streamdetails.item_id}" - self.logger.log(VERBOSE_LOG_LEVEL, f"Start streaming {spotify_uri} using librespot") - args = [ - librespot, - "--cache", - CACHE_DIR, - "--system-cache", - self.config_dir, - "--cache-size-limit", - "1G", - "--passthrough", - "--bitrate", - "320", - "--backend", - "pipe", - "--single-track", - spotify_uri, - "--disable-discovery", - "--dither", - "none", - ] - if seek_position: - args += ["--start-position", str(int(seek_position))] - chunk_size = get_chunksize(streamdetails.audio_format) - stderr = None if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else False - bytes_received = 0 - async with AsyncProcess( - args, - stdout=True, - stderr=stderr, - name="librespot", - ) as librespot_proc: - async for chunk in librespot_proc.iter_any(chunk_size): - yield chunk - bytes_received += len(chunk) - - if librespot_proc.returncode != 0 or bytes_received == 0: - raise AudioError(f"Failed to stream track {spotify_uri}") - - def _parse_artist(self, artist_obj): - """Parse spotify artist object to generic layout.""" - artist = Artist( - item_id=artist_obj["id"], - provider=self.domain, - name=artist_obj["name"] or artist_obj["id"], - provider_mappings={ - ProviderMapping( - item_id=artist_obj["id"], - provider_domain=self.domain, - provider_instance=self.instance_id, - url=artist_obj["external_urls"]["spotify"], - ) - }, - ) - if "genres" in artist_obj: - artist.metadata.genres = set(artist_obj["genres"]) - if artist_obj.get("images"): - for img in artist_obj["images"]: - img_url = img["url"] - if "2a96cbd8b46e442fc41c2b86b821562f" not in img_url: - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img_url, - provider=self.instance_id, - remotely_accessible=True, - ) - ] - break - return artist - - def _parse_album(self, album_obj: dict): - """Parse spotify album object to generic layout.""" - name, version = parse_title_and_version(album_obj["name"]) - album = Album( - item_id=album_obj["id"], - provider=self.domain, - name=name, - version=version, - provider_mappings={ - ProviderMapping( - item_id=album_obj["id"], - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat(content_type=ContentType.OGG, bit_rate=320), - url=album_obj["external_urls"]["spotify"], - ) - }, - ) - if "external_ids" in album_obj and album_obj["external_ids"].get("upc"): - album.external_ids.add((ExternalID.BARCODE, "0" + album_obj["external_ids"]["upc"])) - if "external_ids" in album_obj and album_obj["external_ids"].get("ean"): - album.external_ids.add((ExternalID.BARCODE, album_obj["external_ids"]["ean"])) - - for artist_obj in album_obj["artists"]: - if not artist_obj.get("name") or not artist_obj.get("id"): - continue - album.artists.append(self._parse_artist(artist_obj)) - - with contextlib.suppress(ValueError): - album.album_type = AlbumType(album_obj["album_type"]) - - if "genres" in album_obj: - album.metadata.genre = set(album_obj["genres"]) - if album_obj.get("images"): - album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=album_obj["images"][0]["url"], - provider=self.instance_id, - remotely_accessible=True, - ) - ] - if "label" in album_obj: - album.metadata.label = album_obj["label"] - if album_obj.get("release_date"): - album.year = int(album_obj["release_date"].split("-")[0]) - if album_obj.get("copyrights"): - album.metadata.copyright = album_obj["copyrights"][0]["text"] - if album_obj.get("explicit"): - album.metadata.explicit = album_obj["explicit"] - return album - - def _parse_track( - self, - track_obj: dict[str, Any], - artist=None, - ) -> Track: - """Parse spotify track object to generic layout.""" - name, version = parse_title_and_version(track_obj["name"]) - track = Track( - item_id=track_obj["id"], - provider=self.domain, - name=name, - version=version, - duration=track_obj["duration_ms"] / 1000, - provider_mappings={ - ProviderMapping( - item_id=track_obj["id"], - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.OGG, - bit_rate=320, - ), - url=track_obj["external_urls"]["spotify"], - available=not track_obj["is_local"] and track_obj["is_playable"], - ) - }, - disc_number=track_obj.get("disc_number", 0), - track_number=track_obj.get("track_number", 0), - ) - if isrc := track_obj.get("external_ids", {}).get("isrc"): - track.external_ids.add((ExternalID.ISRC, isrc)) - - if artist: - track.artists.append(artist) - for track_artist in track_obj.get("artists", []): - if not track_artist.get("name") or not track_artist.get("id"): - continue - artist = self._parse_artist(track_artist) - if artist and artist.item_id not in {x.item_id for x in track.artists}: - track.artists.append(artist) - - track.metadata.explicit = track_obj["explicit"] - if "preview_url" in track_obj: - track.metadata.preview = track_obj["preview_url"] - if "album" in track_obj: - track.album = self._parse_album(track_obj["album"]) - if track_obj["album"].get("images"): - track.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=track_obj["album"]["images"][0]["url"], - provider=self.instance_id, - remotely_accessible=True, - ) - ] - if track_obj.get("copyright"): - track.metadata.copyright = track_obj["copyright"] - if track_obj.get("explicit"): - track.metadata.explicit = True - if track_obj.get("popularity"): - track.metadata.popularity = track_obj["popularity"] - return track - - def _parse_playlist(self, playlist_obj): - """Parse spotify playlist object to generic layout.""" - playlist = Playlist( - item_id=playlist_obj["id"], - provider=self.domain, - name=playlist_obj["name"], - owner=playlist_obj["owner"]["display_name"], - provider_mappings={ - ProviderMapping( - item_id=playlist_obj["id"], - provider_domain=self.domain, - provider_instance=self.instance_id, - url=playlist_obj["external_urls"]["spotify"], - ) - }, - ) - playlist.is_editable = ( - playlist_obj["owner"]["id"] == self._sp_user["id"] or playlist_obj["collaborative"] - ) - if playlist_obj.get("images"): - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=playlist_obj["images"][0]["url"], - provider=self.instance_id, - remotely_accessible=True, - ) - ] - if playlist.owner is None: - playlist.owner = self._sp_user["display_name"] - playlist.cache_checksum = str(playlist_obj["snapshot_id"]) - return playlist - - @lock - async def login(self, force_refresh: bool = False) -> dict: - """Log-in Spotify and return Auth/token info.""" - # return existing token if we have one in memory - if ( - not force_refresh - and self._auth_info - and (self._auth_info["expires_at"] > (time.time() - 600)) - ): - return self._auth_info - # request new access token using the refresh token - if not (refresh_token := self.config.get_value(CONF_REFRESH_TOKEN)): - raise LoginFailed("Authentication required") - - client_id = self.config.get_value(CONF_CLIENT_ID) or app_var(2) - params = { - "grant_type": "refresh_token", - "refresh_token": refresh_token, - "client_id": client_id, - } - for _ in range(2): - async with self.mass.http_session.post( - "https://accounts.spotify.com/api/token", data=params - ) as response: - if response.status != 200: - err = await response.text() - if "revoked" in err: - # clear refresh token if it's invalid - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_REFRESH_TOKEN, "" - ) - raise LoginFailed(f"Failed to refresh access token: {err}") - # the token failed to refresh, we allow one retry - await asyncio.sleep(2) - continue - # if we reached this point, the token has been successfully refreshed - auth_info = await response.json() - auth_info["expires_at"] = int(auth_info["expires_in"] + time.time()) - self.logger.debug("Successfully refreshed access token") - break - else: - raise LoginFailed(f"Failed to refresh access token: {err}") - - # make sure that our updated creds get stored in memory + config - self._auth_info = auth_info - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_REFRESH_TOKEN, auth_info["refresh_token"], encrypted=True - ) - # check if librespot still has valid auth - librespot = await self.get_librespot_binary() - args = [ - librespot, - "--system-cache", - self.config_dir, - "--check-auth", - ] - ret_code, stdout = await check_output(*args) - if ret_code != 0: - # cached librespot creds are invalid, re-authenticate - # we can use the check-token option to send a new token to librespot - # librespot will then get its own token from spotify (somehow) and cache that. - args = [ - librespot, - "--system-cache", - self.config_dir, - "--check-auth", - "--access-token", - auth_info["access_token"], - ] - ret_code, stdout = await check_output(*args) - if ret_code != 0: - # this should not happen, but guard it just in case - err = stdout.decode("utf-8").strip() - raise LoginFailed(f"Failed to verify credentials on Librespot: {err}") - - # get logged-in user info - if not self._sp_user: - self._sp_user = userinfo = await self._get_data("me", auth_info=auth_info) - self.mass.metadata.set_default_preferred_language(userinfo["country"]) - self.logger.info("Successfully logged in to Spotify as %s", userinfo["display_name"]) - return auth_info - - async def _get_all_items(self, endpoint, key="items", **kwargs) -> list[dict]: - """Get all items from a paged list.""" - limit = 50 - offset = 0 - all_items = [] - while True: - kwargs["limit"] = limit - kwargs["offset"] = offset - result = await self._get_data(endpoint, **kwargs) - offset += limit - if not result or key not in result or not result[key]: - break - all_items += result[key] - if len(result[key]) < limit: - break - return all_items - - @throttle_with_retries - async def _get_data(self, endpoint, **kwargs) -> dict[str, Any]: - """Get data from api.""" - url = f"https://api.spotify.com/v1/{endpoint}" - kwargs["market"] = "from_token" - kwargs["country"] = "from_token" - if not (auth_info := kwargs.pop("auth_info", None)): - auth_info = await self.login() - headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} - locale = self.mass.metadata.locale.replace("_", "-") - language = locale.split("-")[0] - headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" - async with ( - self.mass.http_session.get( - url, headers=headers, params=kwargs, ssl=True, timeout=120 - ) as response, - ): - # handle spotify rate limiter - if response.status == 429: - backoff_time = int(response.headers["Retry-After"]) - raise ResourceTemporarilyUnavailable( - "Spotify Rate Limiter", backoff_time=backoff_time - ) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - - # handle token expired, raise ResourceTemporarilyUnavailable - # so it will be retried (and the token refreshed) - if response.status == 401: - self._auth_info = None - raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) - - # handle 404 not found, convert to MediaNotFoundError - if response.status == 404: - raise MediaNotFoundError(f"{endpoint} not found") - response.raise_for_status() - return await response.json(loads=json_loads) - - @throttle_with_retries - async def _delete_data(self, endpoint, data=None, **kwargs) -> None: - """Delete data from api.""" - url = f"https://api.spotify.com/v1/{endpoint}" - auth_info = kwargs.pop("auth_info", await self.login()) - headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} - async with self.mass.http_session.delete( - url, headers=headers, params=kwargs, json=data, ssl=False - ) as response: - # handle spotify rate limiter - if response.status == 429: - backoff_time = int(response.headers["Retry-After"]) - raise ResourceTemporarilyUnavailable( - "Spotify Rate Limiter", backoff_time=backoff_time - ) - # handle token expired, raise ResourceTemporarilyUnavailable - # so it will be retried (and the token refreshed) - if response.status == 401: - self._auth_info = None - raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - response.raise_for_status() - - @throttle_with_retries - async def _put_data(self, endpoint, data=None, **kwargs) -> None: - """Put data on api.""" - url = f"https://api.spotify.com/v1/{endpoint}" - auth_info = kwargs.pop("auth_info", await self.login()) - headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} - async with self.mass.http_session.put( - url, headers=headers, params=kwargs, json=data, ssl=False - ) as response: - # handle spotify rate limiter - if response.status == 429: - backoff_time = int(response.headers["Retry-After"]) - raise ResourceTemporarilyUnavailable( - "Spotify Rate Limiter", backoff_time=backoff_time - ) - # handle token expired, raise ResourceTemporarilyUnavailable - # so it will be retried (and the token refreshed) - if response.status == 401: - self._auth_info = None - raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) - - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - response.raise_for_status() - - @throttle_with_retries - async def _post_data(self, endpoint, data=None, **kwargs) -> dict[str, Any]: - """Post data on api.""" - url = f"https://api.spotify.com/v1/{endpoint}" - auth_info = kwargs.pop("auth_info", await self.login()) - headers = {"Authorization": f'Bearer {auth_info["access_token"]}'} - async with self.mass.http_session.post( - url, headers=headers, params=kwargs, json=data, ssl=False - ) as response: - # handle spotify rate limiter - if response.status == 429: - backoff_time = int(response.headers["Retry-After"]) - raise ResourceTemporarilyUnavailable( - "Spotify Rate Limiter", backoff_time=backoff_time - ) - # handle token expired, raise ResourceTemporarilyUnavailable - # so it will be retried (and the token refreshed) - if response.status == 401: - self._auth_info = None - raise ResourceTemporarilyUnavailable("Token expired", backoff_time=0.05) - # handle temporary server error - if response.status in (502, 503): - raise ResourceTemporarilyUnavailable(backoff_time=30) - response.raise_for_status() - return await response.json(loads=json_loads) - - async def get_librespot_binary(self): - """Find the correct librespot binary belonging to the platform.""" - # ruff: noqa: SIM102 - if self._librespot_bin is not None: - return self._librespot_bin - - async def check_librespot(librespot_path: str) -> str | None: - try: - returncode, output = await check_output(librespot_path, "--version") - if returncode == 0 and b"librespot" in output: - self._librespot_bin = librespot_path - return librespot_path - except OSError: - return None - - base_path = os.path.join(os.path.dirname(__file__), "bin") - system = platform.system().lower().replace("darwin", "macos") - architecture = platform.machine().lower() - - if bridge_binary := await check_librespot( - os.path.join(base_path, f"librespot-{system}-{architecture}") - ): - return bridge_binary - - msg = f"Unable to locate Librespot for {system}/{architecture}" - raise RuntimeError(msg) - - def _fix_create_playlist_api_bug(self, playlist_obj: dict[str, Any]) -> None: - """Fix spotify API bug where incorrect owner id is returned from Create Playlist.""" - if playlist_obj["owner"]["id"] != self._sp_user["id"]: - playlist_obj["owner"]["id"] = self._sp_user["id"] - playlist_obj["owner"]["display_name"] = self._sp_user["display_name"] - else: - self.logger.warning( - "FIXME: Spotify have fixed their Create Playlist API, this fix can be removed." - ) diff --git a/music_assistant/server/providers/spotify/bin/librespot-linux-aarch64 b/music_assistant/server/providers/spotify/bin/librespot-linux-aarch64 deleted file mode 100755 index 7b91c8ef..00000000 Binary files a/music_assistant/server/providers/spotify/bin/librespot-linux-aarch64 and /dev/null differ diff --git a/music_assistant/server/providers/spotify/bin/librespot-linux-x86_64 b/music_assistant/server/providers/spotify/bin/librespot-linux-x86_64 deleted file mode 100755 index 1022c144..00000000 Binary files a/music_assistant/server/providers/spotify/bin/librespot-linux-x86_64 and /dev/null differ diff --git a/music_assistant/server/providers/spotify/bin/librespot-macos-arm64 b/music_assistant/server/providers/spotify/bin/librespot-macos-arm64 deleted file mode 100755 index de3c183b..00000000 Binary files a/music_assistant/server/providers/spotify/bin/librespot-macos-arm64 and /dev/null differ diff --git a/music_assistant/server/providers/spotify/icon.svg b/music_assistant/server/providers/spotify/icon.svg deleted file mode 100644 index 843cf994..00000000 --- a/music_assistant/server/providers/spotify/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/music_assistant/server/providers/spotify/manifest.json b/music_assistant/server/providers/spotify/manifest.json deleted file mode 100644 index 01574ac6..00000000 --- a/music_assistant/server/providers/spotify/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "spotify", - "name": "Spotify", - "description": "Support for the Spotify streaming provider in Music Assistant.", - "codeowners": ["@music-assistant"], - "requirements": ["pkce==1.0.3"], - "documentation": "https://music-assistant.io/music-providers/spotify/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/test/__init__.py b/music_assistant/server/providers/test/__init__.py deleted file mode 100644 index db3eabb6..00000000 --- a/music_assistant/server/providers/test/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Test/Demo provider that creates a collection of fake media items.""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( - ContentType, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - MediaItemImage, - MediaItemMetadata, - ProviderMapping, - Track, - UniqueList, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import MASS_LOGO, VARIOUS_ARTISTS_FANART -from music_assistant.server.models.music_provider import MusicProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -DEFAULT_THUMB = MediaItemImage( - type=ImageType.THUMB, - path=MASS_LOGO, - provider="builtin", - remotely_accessible=False, -) - -DEFAULT_FANART = MediaItemImage( - type=ImageType.FANART, - path=VARIOUS_ARTISTS_FANART, - provider="builtin", - remotely_accessible=False, -) - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return TestProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, # noqa: ARG001 - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, # noqa: ARG001 - values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return () - - -class TestProvider(MusicProvider): - """Test/Demo provider that creates a collection of fake media items.""" - - @property - def is_streaming_provider(self) -> bool: - """Return True if the provider is a streaming provider.""" - return False - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return (ProviderFeature.LIBRARY_TRACKS,) - - async def get_track(self, prov_track_id: str) -> Track: - """Get full track details by id.""" - artist_idx, album_idx, track_idx = prov_track_id.split("_", 3) - return Track( - item_id=prov_track_id, - provider=self.instance_id, - name=f"Test Track {artist_idx} - {album_idx} - {track_idx}", - duration=5, - artists=UniqueList([await self.get_artist(artist_idx)]), - album=await self.get_album(f"{artist_idx}_{album_idx}"), - provider_mappings={ - ProviderMapping( - item_id=prov_track_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ), - }, - metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])), - disc_number=1, - track_number=int(track_idx), - ) - - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get full artist details by id.""" - return Artist( - item_id=prov_artist_id, - provider=self.instance_id, - name=f"Test Artist {prov_artist_id}", - metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB, DEFAULT_FANART])), - provider_mappings={ - ProviderMapping( - item_id=prov_artist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - ) - - async def get_album(self, prov_album_id: str) -> Album: - """Get full artist details by id.""" - artist_idx, album_idx = prov_album_id.split("_", 2) - return Album( - item_id=prov_album_id, - provider=self.instance_id, - name=f"Test Album {album_idx}", - artists=UniqueList([await self.get_artist(artist_idx)]), - provider_mappings={ - ProviderMapping( - item_id=prov_album_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - ) - }, - metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])), - ) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from the provider.""" - for artist_idx in range(50): - for album_idx in range(25): - for track_idx in range(25): - track_item_id = f"{artist_idx}_{album_idx}_{track_idx}" - yield await self.get_track(track_item_id) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a track/radio.""" - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=AudioFormat( - content_type=ContentType.MP3, - sample_rate=44100, - bit_depth=16, - channels=2, - ), - media_type=MediaType.TRACK, - stream_type=StreamType.HTTP, - path=item_id, - can_seek=True, - ) diff --git a/music_assistant/server/providers/test/icon.svg b/music_assistant/server/providers/test/icon.svg deleted file mode 100644 index 845920ca..00000000 --- a/music_assistant/server/providers/test/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/music_assistant/server/providers/test/manifest.json b/music_assistant/server/providers/test/manifest.json deleted file mode 100644 index a8cd64d7..00000000 --- a/music_assistant/server/providers/test/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "music", - "domain": "test", - "name": "Test / demo provider", - "description": "Test/Demo provider that creates a collection of fake media items.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "", - "multi_instance": false, - "builtin": false -} diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/server/providers/theaudiodb/__init__.py deleted file mode 100644 index c786aaa9..00000000 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ /dev/null @@ -1,415 +0,0 @@ -"""The AudioDB Metadata provider for Music Assistant.""" - -from __future__ import annotations - -from json import JSONDecodeError -from typing import TYPE_CHECKING, Any, cast - -import aiohttp.client_exceptions - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - ImageType, - LinkType, - MediaItemImage, - MediaItemLink, - MediaItemMetadata, - Track, - UniqueList, -) -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.app_vars import app_var # type: ignore[attr-defined] -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.metadata_provider import MetadataProvider - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -SUPPORTED_FEATURES = ( - ProviderFeature.ARTIST_METADATA, - ProviderFeature.ALBUM_METADATA, - ProviderFeature.TRACK_METADATA, -) - -IMG_MAPPING = { - "strArtistThumb": ImageType.THUMB, - "strArtistLogo": ImageType.LOGO, - "strArtistCutout": ImageType.CUTOUT, - "strArtistClearart": ImageType.CLEARART, - "strArtistWideThumb": ImageType.LANDSCAPE, - "strArtistFanart": ImageType.FANART, - "strArtistBanner": ImageType.BANNER, - "strAlbumThumb": ImageType.THUMB, - "strAlbumThumbHQ": ImageType.THUMB, - "strAlbumCDart": ImageType.DISCART, - "strAlbum3DCase": ImageType.OTHER, - "strAlbum3DFlat": ImageType.OTHER, - "strAlbum3DFace": ImageType.OTHER, - "strAlbum3DThumb": ImageType.OTHER, - "strTrackThumb": ImageType.THUMB, - "strTrack3DCase": ImageType.OTHER, -} - -LINK_MAPPING = { - "strWebsite": LinkType.WEBSITE, - "strFacebook": LinkType.FACEBOOK, - "strTwitter": LinkType.TWITTER, - "strLastFMChart": LinkType.LASTFM, -} - -ALBUMTYPE_MAPPING = { - "Single": AlbumType.SINGLE, - "Compilation": AlbumType.COMPILATION, - "Album": AlbumType.ALBUM, - "EP": AlbumType.EP, -} - -CONF_ENABLE_IMAGES = "enable_images" -CONF_ENABLE_ARTIST_METADATA = "enable_artist_metadata" -CONF_ENABLE_ALBUM_METADATA = "enable_album_metadata" -CONF_ENABLE_TRACK_METADATA = "enable_track_metadata" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return AudioDbMetadataProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_ENABLE_ARTIST_METADATA, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of artist metadata.", - default_value=True, - ), - ConfigEntry( - key=CONF_ENABLE_ALBUM_METADATA, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of album metadata.", - default_value=True, - ), - ConfigEntry( - key=CONF_ENABLE_TRACK_METADATA, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of track metadata.", - default_value=False, - ), - ConfigEntry( - key=CONF_ENABLE_IMAGES, - type=ConfigEntryType.BOOLEAN, - label="Enable retrieval of artist/album/track images", - default_value=True, - ), - ) - - -class AudioDbMetadataProvider(MetadataProvider): - """The AudioDB Metadata provider.""" - - throttler: Throttler - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self.cache = self.mass.cache - self.throttler = Throttler(rate_limit=1, period=1) - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: - """Retrieve metadata for artist on theaudiodb.""" - if not self.config.get_value(CONF_ENABLE_ARTIST_METADATA): - return None - if not artist.mbid: - # for 100% accuracy we require the musicbrainz id for all lookups - return None - if data := await self._get_data("artist-mb.php", i=artist.mbid): - if data.get("artists"): - return self.__parse_artist(data["artists"][0]) - return None - - async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: - """Retrieve metadata for album on theaudiodb.""" - if not self.config.get_value(CONF_ENABLE_ALBUM_METADATA): - return None - if mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP): - result = await self._get_data("album-mb.php", i=mbid) - if result and result.get("album"): - adb_album = result["album"][0] - return await self.__parse_album(album, adb_album) - # if there was no match on mbid, there will certainly be no match by name - return None - # fallback if no musicbrainzid: lookup by name - for album_artist in album.artists: - # make sure to include the version in the album name - album_name = f"{album.name} {album.version}" if album.version else album.name - result = await self._get_data("searchalbum.php?", s=album_artist.name, a=album_name) - if result and result.get("album"): - for item in result["album"]: - # some safety checks - if album_artist.mbid: - if album_artist.mbid != item["strMusicBrainzArtistID"]: - continue - elif not compare_strings(album_artist.name, item["strArtist"]): - continue - if compare_strings(album_name, item["strAlbum"], strict=False): - # match found ! - return await self.__parse_album(album, item) - return None - - async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None: - """Retrieve metadata for track on theaudiodb.""" - if not self.config.get_value(CONF_ENABLE_TRACK_METADATA): - return None - if track.mbid: - result = await self._get_data("track-mb.php", i=track.mbid) - if result and result.get("track"): - return await self.__parse_track(track, result["track"][0]) - # if there was no match on mbid, there will certainly be no match by name - return None - # fallback if no musicbrainzid: lookup by name - for track_artist in track.artists: - # make sure to include the version in the album name - track_name = f"{track.name} {track.version}" if track.version else track.name - result = await self._get_data("searchtrack.php?", s=track_artist.name, t=track_name) - if result and result.get("track"): - for item in result["track"]: - # some safety checks - if track_artist.mbid: - if track_artist.mbid != item["strMusicBrainzArtistID"]: - continue - elif not compare_strings(track_artist.name, item["strArtist"]): - continue - if ( # noqa: SIM114 - track.album - and (mb_rgid := track.album.get_external_id(ExternalID.MB_RELEASEGROUP)) - # AudioDb swapped MB Album ID and ReleaseGroup ID ?! - and mb_rgid != item["strMusicBrainzAlbumID"] - ): - continue - elif track.album and not compare_strings( - track.album.name, item["strAlbum"], strict=False - ): - continue - if not compare_strings(track_name, item["strTrack"], strict=False): - continue - return await self.__parse_track(track, item) - return None - - def __parse_artist(self, artist_obj: dict[str, Any]) -> MediaItemMetadata: - """Parse audiodb artist object to MediaItemMetadata.""" - metadata = MediaItemMetadata() - # generic data - metadata.label = artist_obj.get("strLabel") - metadata.style = artist_obj.get("strStyle") - if genre := artist_obj.get("strGenre"): - metadata.genres = {genre} - metadata.mood = artist_obj.get("strMood") - # links - metadata.links = set() - for key, link_type in LINK_MAPPING.items(): - if link := artist_obj.get(key): - metadata.links.add(MediaItemLink(type=link_type, url=link)) - # description/biography - lang_code, lang_country = self.mass.metadata.locale.split("_") - if desc := artist_obj.get(f"strBiography{lang_country}") or ( - desc := artist_obj.get(f"strBiography{lang_code.upper()}") - ): - metadata.description = desc - else: - metadata.description = artist_obj.get("strBiographyEN") - # images - if not self.config.get_value(CONF_ENABLE_IMAGES): - return metadata - metadata.images = UniqueList() - for key, img_type in IMG_MAPPING.items(): - for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): - if img := artist_obj.get(f"{key}{postfix}"): - metadata.images.append( - MediaItemImage( - type=img_type, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ) - else: - break - return metadata - - async def __parse_album(self, album: Album, adb_album: dict[str, Any]) -> MediaItemMetadata: - """Parse audiodb album object to MediaItemMetadata.""" - metadata = MediaItemMetadata() - # generic data - metadata.label = adb_album.get("strLabel") - metadata.style = adb_album.get("strStyle") - if genre := adb_album.get("strGenre"): - metadata.genres = {genre} - metadata.mood = adb_album.get("strMood") - # links - metadata.links = set() - if link := adb_album.get("strWikipediaID"): - metadata.links.add( - MediaItemLink(type=LinkType.WIKIPEDIA, url=f"https://wikipedia.org/wiki/{link}") - ) - if link := adb_album.get("strAllMusicID"): - metadata.links.add( - MediaItemLink(type=LinkType.ALLMUSIC, url=f"https://www.allmusic.com/album/{link}") - ) - - # description - lang_code, lang_country = self.mass.metadata.locale.split("_") - if desc := adb_album.get(f"strDescription{lang_country}") or ( - desc := adb_album.get(f"strDescription{lang_code.upper()}") - ): - metadata.description = desc - else: - metadata.description = adb_album.get("strDescriptionEN") - metadata.review = adb_album.get("strReview") - # images - if not self.config.get_value(CONF_ENABLE_IMAGES): - return metadata - metadata.images = UniqueList() - for key, img_type in IMG_MAPPING.items(): - for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): - if img := adb_album.get(f"{key}{postfix}"): - metadata.images.append( - MediaItemImage( - type=img_type, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ) - else: - break - # fill in some missing album info if needed - if not album.year: - album.year = int(adb_album.get("intYearReleased", "0")) - if album.album_type == AlbumType.UNKNOWN and adb_album.get("strReleaseFormat"): - releaseformat = cast(str, adb_album.get("strReleaseFormat")) - album.album_type = ALBUMTYPE_MAPPING.get(releaseformat, AlbumType.UNKNOWN) - # update the artist mbid while at it - for album_artist in album.artists: - if not compare_strings(album_artist.name, adb_album["strArtist"]): - continue - if not album_artist.mbid and album_artist.provider == "library": - album_artist.mbid = adb_album["strMusicBrainzArtistID"] - await self.mass.music.artists.update_item_in_library( - album_artist.item_id, - album_artist, # type: ignore[arg-type] - ) - return metadata - - async def __parse_track(self, track: Track, adb_track: dict[str, Any]) -> MediaItemMetadata: - """Parse audiodb track object to MediaItemMetadata.""" - metadata = MediaItemMetadata() - # generic data - metadata.lyrics = adb_track.get("strTrackLyrics") - metadata.style = adb_track.get("strStyle") - if genre := adb_track.get("strGenre"): - metadata.genres = {genre} - metadata.mood = adb_track.get("strMood") - # description - lang_code, lang_country = self.mass.metadata.locale.split("_") - if desc := adb_track.get(f"strDescription{lang_country}") or ( - desc := adb_track.get(f"strDescription{lang_code.upper()}") - ): - metadata.description = desc - else: - metadata.description = adb_track.get("strDescriptionEN") - # images - if not self.config.get_value(CONF_ENABLE_IMAGES): - return metadata - metadata.images = UniqueList([]) - for key, img_type in IMG_MAPPING.items(): - for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): - if img := adb_track.get(f"{key}{postfix}"): - metadata.images.append( - MediaItemImage( - type=img_type, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ) - else: - break - # update the artist mbid while at it - for album_artist in track.artists: - if not compare_strings(album_artist.name, adb_track["strArtist"]): - continue - if not album_artist.mbid and album_artist.provider == "library": - album_artist.mbid = adb_track["strMusicBrainzArtistID"] - await self.mass.music.artists.update_item_in_library( - album_artist.item_id, - album_artist, # type: ignore[arg-type] - ) - # update the album mbid while at it - if ( - track.album - and not track.album.get_external_id(ExternalID.MB_RELEASEGROUP) - and track.album.provider == "library" - and isinstance(track.album, Album) - ): - track.album.add_external_id( - ExternalID.MB_RELEASEGROUP, adb_track["strMusicBrainzAlbumID"] - ) - await self.mass.music.albums.update_item_in_library(track.album.item_id, track.album) - return metadata - - @use_cache(86400 * 30) - async def _get_data(self, endpoint: str, **kwargs: Any) -> dict[str, Any] | None: - """Get data from api.""" - url = f"https://theaudiodb.com/api/v1/json/{app_var(3)}/{endpoint}" - async with ( - self.throttler, - self.mass.http_session.get(url, params=kwargs, ssl=False) as response, - ): - try: - result = cast(dict[str, Any], await response.json()) - except ( - aiohttp.client_exceptions.ContentTypeError, - JSONDecodeError, - ): - self.logger.error("Failed to retrieve %s", endpoint) - text_result = await response.text() - self.logger.debug(text_result) - return None - except ( - aiohttp.client_exceptions.ClientConnectorError, - aiohttp.client_exceptions.ServerDisconnectedError, - TimeoutError, - ): - self.logger.warning("Failed to retrieve %s", endpoint) - return None - if "error" in result and "limit" in result["error"]: - self.logger.warning(result["error"]) - return None - return result diff --git a/music_assistant/server/providers/theaudiodb/manifest.json b/music_assistant/server/providers/theaudiodb/manifest.json deleted file mode 100644 index 9b2eaecf..00000000 --- a/music_assistant/server/providers/theaudiodb/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "metadata", - "domain": "theaudiodb", - "name": "The Audio DB", - "description": "TheAudioDB is a community Database of audio artwork and metadata with a JSON API.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "", - "multi_instance": false, - "builtin": true, - "icon": "folder-information" -} diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/server/providers/tidal/__init__.py deleted file mode 100644 index f06defca..00000000 --- a/music_assistant/server/providers/tidal/__init__.py +++ /dev/null @@ -1,954 +0,0 @@ -"""Tidal music provider support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import base64 -import pickle -from collections.abc import Callable -from contextlib import suppress -from datetime import datetime, timedelta -from enum import StrEnum -from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast - -from tidalapi import Album as TidalAlbum -from tidalapi import Artist as TidalArtist -from tidalapi import Config as TidalConfig -from tidalapi import Playlist as TidalPlaylist -from tidalapi import Session as TidalSession -from tidalapi import Track as TidalTrack -from tidalapi import exceptions as tidal_exceptions - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - AlbumType, - CacheCategory, - ConfigEntryType, - ExternalID, - ImageType, - MediaType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( - Album, - Artist, - AudioFormat, - ContentType, - ItemMapping, - MediaItemImage, - MediaItemType, - Playlist, - ProviderMapping, - SearchResults, - Track, - UniqueList, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.tags import AudioTags, parse_tags -from music_assistant.server.helpers.throttle_retry import ( - ThrottlerManager, - throttle_with_retries, -) -from music_assistant.server.models.music_provider import MusicProvider - -from .helpers import ( - DEFAULT_LIMIT, - add_playlist_tracks, - create_playlist, - get_album, - get_album_tracks, - get_artist, - get_artist_albums, - get_artist_toptracks, - get_library_albums, - get_library_artists, - get_library_playlists, - get_library_tracks, - get_playlist, - get_playlist_tracks, - get_similar_tracks, - get_stream, - get_track, - library_items_add_remove, - remove_playlist_tracks, - search, -) - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Awaitable - - from tidalapi.media import Lyrics as TidalLyrics - from tidalapi.media import Stream as TidalStream - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - -TOKEN_TYPE = "Bearer" - -# Actions -CONF_ACTION_START_PKCE_LOGIN = "start_pkce_login" -CONF_ACTION_COMPLETE_PKCE_LOGIN = "auth" -CONF_ACTION_CLEAR_AUTH = "clear_auth" - -# Intermediate steps -CONF_TEMP_SESSION = "temp_session" -CONF_OOPS_URL = "oops_url" - -# Config keys -CONF_AUTH_TOKEN = "auth_token" -CONF_REFRESH_TOKEN = "refresh_token" -CONF_USER_ID = "user_id" -CONF_EXPIRY_TIME = "expiry_time" -CONF_QUALITY = "quality" - -# Labels -LABEL_START_PKCE_LOGIN = "start_pkce_login_label" -LABEL_OOPS_URL = "oops_url_label" -LABEL_COMPLETE_PKCE_LOGIN = "complete_pkce_login_label" - -BROWSE_URL = "https://tidal.com/browse" -RESOURCES_URL = "https://resources.tidal.com/images" - -_R = TypeVar("_R") -_P = ParamSpec("_P") - - -class TidalQualityEnum(StrEnum): - """Enum for Tidal Quality.""" - - HIGH_LOSSLESS = "LOSSLESS" # "High - 16bit, 44.1kHz" - HI_RES = "HI_RES_LOSSLESS" # "Max - Up to 24bit, 192kHz" - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return TidalProvider(mass, manifest, config) - - -async def tidal_auth_url(auth_helper: AuthenticationHelper, quality: str) -> str: - """Generate the Tidal authentication URL.""" - - def inner() -> TidalSession: - # global glob_temp_session - config = TidalConfig(quality=quality, item_limit=10000, alac=False) - session = TidalSession(config=config) - url = session.pkce_login_url() - auth_helper.send_url(url) - session_bytes = pickle.dumps(session) - base64_bytes = base64.b64encode(session_bytes) - return base64_bytes.decode("utf-8") - - return await asyncio.to_thread(inner) - - -async def tidal_pkce_login(base64_session: str, url: str) -> TidalSession: - """Async wrapper around the tidalapi Session function.""" - - def inner() -> TidalSession: - base64_bytes = base64_session.encode("utf-8") - message_bytes = base64.b64decode(base64_bytes) - session = pickle.loads(message_bytes) # noqa: S301 - token = session.pkce_get_auth_token(url_redirect=url) - session.process_auth_token(token) - return session - - return await asyncio.to_thread(inner) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - assert values is not None - - if action == CONF_ACTION_START_PKCE_LOGIN: - async with AuthenticationHelper(mass, cast(str, values["session_id"])) as auth_helper: - quality = str(values.get(CONF_QUALITY)) - base64_session = await tidal_auth_url(auth_helper, quality) - values[CONF_TEMP_SESSION] = base64_session - # Tidal is (ab)using the AuthenticationHelper just to send the user to an URL - # there is no actual oauth callback happening, instead the user is redirected - # to a non-existent page and needs to copy the URL from the browser and paste it - # we simply wait here to allow the user to start the auth - await asyncio.sleep(15) - - if action == CONF_ACTION_COMPLETE_PKCE_LOGIN: - quality = str(values.get(CONF_QUALITY)) - pkce_url = str(values.get(CONF_OOPS_URL)) - base64_session = str(values.get(CONF_TEMP_SESSION)) - tidal_session = await tidal_pkce_login(base64_session, pkce_url) - if not tidal_session.check_login(): - msg = "Authentication to Tidal failed" - raise LoginFailed(msg) - # set the retrieved token on the values object to pass along - values[CONF_AUTH_TOKEN] = tidal_session.access_token - values[CONF_REFRESH_TOKEN] = tidal_session.refresh_token - values[CONF_EXPIRY_TIME] = tidal_session.expiry_time.isoformat() - values[CONF_USER_ID] = str(tidal_session.user.id) - values[CONF_TEMP_SESSION] = "" - - if action == CONF_ACTION_CLEAR_AUTH: - values[CONF_AUTH_TOKEN] = None - - if values.get(CONF_AUTH_TOKEN): - auth_entries: tuple[ConfigEntry, ...] = ( - ConfigEntry( - key="label_ok", - type=ConfigEntryType.LABEL, - label="You are authenticated with Tidal", - ), - ConfigEntry( - key=CONF_ACTION_CLEAR_AUTH, - type=ConfigEntryType.ACTION, - label="Reset authentication", - description="Reset the authentication for Tidal", - action=CONF_ACTION_CLEAR_AUTH, - value=None, - ), - ConfigEntry( - key=CONF_QUALITY, - type=ConfigEntryType.STRING, - label=CONF_QUALITY, - required=True, - hidden=True, - default_value=values.get(CONF_QUALITY, TidalQualityEnum.HI_RES.value), - value=values.get(CONF_QUALITY), - ), - ) - else: - auth_entries = ( - ConfigEntry( - key=CONF_QUALITY, - type=ConfigEntryType.STRING, - label="Quality setting for Tidal:", - required=True, - description="HIGH_LOSSLESS = 16bit 44.1kHz, HI_RES = Up to 24bit 192kHz", - options=tuple(ConfigValueOption(x.value, x.name) for x in TidalQualityEnum), - default_value=TidalQualityEnum.HI_RES.value, - value=values.get(CONF_QUALITY) if values else None, - ), - ConfigEntry( - key=LABEL_START_PKCE_LOGIN, - type=ConfigEntryType.LABEL, - label="The button below will redirect you to Tidal.com to authenticate.\n\n" - " After authenticating, you will be redirected to a page that prominently displays" - " 'Oops' at the top. That is normal, you need to copy that URL from the " - "address bar and come back here", - hidden=action == CONF_ACTION_START_PKCE_LOGIN, - ), - ConfigEntry( - key=CONF_ACTION_START_PKCE_LOGIN, - type=ConfigEntryType.ACTION, - label="Starts the auth process via PKCE on Tidal.com", - description="This button will redirect you to Tidal.com to authenticate." - " After authenticating, you will be redirected to a page that prominently displays" - " 'Oops' at the top.", - action=CONF_ACTION_START_PKCE_LOGIN, - depends_on=CONF_QUALITY, - action_label="Starts the auth process via PKCE on Tidal.com", - value=values.get(CONF_TEMP_SESSION) if values else None, - hidden=action == CONF_ACTION_START_PKCE_LOGIN, - ), - ConfigEntry( - key=CONF_TEMP_SESSION, - type=ConfigEntryType.STRING, - label="Temporary session for Tidal", - hidden=True, - required=False, - value=values.get(CONF_TEMP_SESSION) if values else None, - ), - ConfigEntry( - key=LABEL_OOPS_URL, - type=ConfigEntryType.LABEL, - label="Copy the URL from the 'Oops' page that you were previously redirected to" - " and paste it in the field below", - hidden=action != CONF_ACTION_START_PKCE_LOGIN, - ), - ConfigEntry( - key=CONF_OOPS_URL, - type=ConfigEntryType.STRING, - label="Oops URL from Tidal redirect", - description="This field should be filled manually by you after authenticating on" - " Tidal.com and being redirected to a page that prominently displays" - " 'Oops' at the top.", - depends_on=CONF_ACTION_START_PKCE_LOGIN, - value=values.get(CONF_OOPS_URL) if values else None, - hidden=action != CONF_ACTION_START_PKCE_LOGIN, - ), - ConfigEntry( - key=LABEL_COMPLETE_PKCE_LOGIN, - type=ConfigEntryType.LABEL, - label="After pasting the URL in the field above, click the button below to complete" - " the process.", - hidden=action != CONF_ACTION_START_PKCE_LOGIN, - ), - ConfigEntry( - key=CONF_ACTION_COMPLETE_PKCE_LOGIN, - type=ConfigEntryType.ACTION, - label="Complete the auth process via PKCE on Tidal.com", - description="Click this after adding the 'Oops' URL above, this will complete the" - " authentication process.", - action=CONF_ACTION_COMPLETE_PKCE_LOGIN, - depends_on=CONF_OOPS_URL, - action_label="Complete the auth process via PKCE on Tidal.com", - value=None, - hidden=action != CONF_ACTION_START_PKCE_LOGIN, - ), - ) - - # return the collected config entries - return ( - *auth_entries, - ConfigEntry( - key=CONF_AUTH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Authentication token for Tidal", - description="You need to link Music Assistant to your Tidal account.", - hidden=True, - value=values.get(CONF_AUTH_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_REFRESH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Refresh token for Tidal", - description="You need to link Music Assistant to your Tidal account.", - hidden=True, - value=values.get(CONF_REFRESH_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_EXPIRY_TIME, - type=ConfigEntryType.STRING, - label="Expiry time of auth token for Tidal", - hidden=True, - value=values.get(CONF_EXPIRY_TIME) if values else None, - ), - ConfigEntry( - key=CONF_USER_ID, - type=ConfigEntryType.STRING, - label="Your Tidal User ID", - description="This is your unique Tidal user ID.", - hidden=True, - value=values.get(CONF_USER_ID) if values else None, - ), - ) - - -class TidalProvider(MusicProvider): - """Implementation of a Tidal MusicProvider.""" - - _tidal_session: TidalSession | None = None - _tidal_user_id: str - # rate limiter needs to be specified on provider-level, - # so make it an instance attribute - throttler = ThrottlerManager(rate_limit=1, period=2) - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self._tidal_user_id = str(self.config.get_value(CONF_USER_ID)) - try: - self._tidal_session = await self._get_tidal_session() - except Exception as err: - if "401 Client Error: Unauthorized" in str(err): - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_AUTH_TOKEN, None - ) - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_REFRESH_TOKEN, None - ) - raise LoginFailed("Credentials expired, you need to re-setup") - raise - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SEARCH, - ProviderFeature.LIBRARY_ARTISTS_EDIT, - ProviderFeature.LIBRARY_ALBUMS_EDIT, - ProviderFeature.LIBRARY_TRACKS_EDIT, - ProviderFeature.LIBRARY_PLAYLISTS_EDIT, - ProviderFeature.PLAYLIST_CREATE, - ProviderFeature.SIMILAR_TRACKS, - ProviderFeature.BROWSE, - ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) - - async def search( - self, - search_query: str, - media_types: list[MediaType], - limit: int = 5, - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: Number of items to return in the search (per type). - """ - parsed_results = SearchResults() - media_types = [ - x - for x in media_types - if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) - ] - if not media_types: - return parsed_results - - tidal_session = await self._get_tidal_session() - search_query = search_query.replace("'", "") - results = await search(tidal_session, search_query, media_types, limit) - - if results["artists"]: - parsed_results.artists = [self._parse_artist(artist) for artist in results["artists"]] - if results["albums"]: - parsed_results.albums = [self._parse_album(album) for album in results["albums"]] - if results["playlists"]: - parsed_results.playlists = [ - self._parse_playlist(playlist) for playlist in results["playlists"] - ] - if results["tracks"]: - parsed_results.tracks = [self._parse_track(track) for track in results["tracks"]] - return parsed_results - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Tidal.""" - tidal_session = await self._get_tidal_session() - artist: TidalArtist # satisfy the type checker - async for artist in self._iter_items( - get_library_artists, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT - ): - yield self._parse_artist(artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Tidal.""" - tidal_session = await self._get_tidal_session() - album: TidalAlbum # satisfy the type checker - async for album in self._iter_items( - get_library_albums, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT - ): - yield self._parse_album(album) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Tidal.""" - tidal_session = await self._get_tidal_session() - track: TidalTrack # satisfy the type checker - async for track in self._iter_items( - get_library_tracks, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT - ): - yield self._parse_track(track) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from the provider.""" - tidal_session = await self._get_tidal_session() - playlist: TidalPlaylist # satisfy the type checker - async for playlist in self._iter_items( - get_library_playlists, tidal_session, self._tidal_user_id - ): - yield self._parse_playlist(playlist) - - @throttle_with_retries - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get album tracks for given album id.""" - tidal_session = await self._get_tidal_session() - tracks_obj = await get_album_tracks(tidal_session, prov_album_id) - return [self._parse_track(track_obj=track_obj) for track_obj in tracks_obj] - - @throttle_with_retries - async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: - """Get a list of all albums for the given artist.""" - tidal_session = await self._get_tidal_session() - artist_albums_obj = await get_artist_albums(tidal_session, prov_artist_id) - return [self._parse_album(album) for album in artist_albums_obj] - - @throttle_with_retries - async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: - """Get a list of 10 most popular tracks for the given artist.""" - tidal_session = await self._get_tidal_session() - try: - artist_toptracks_obj = await get_artist_toptracks(tidal_session, prov_artist_id) - return [self._parse_track(track) for track in artist_toptracks_obj] - except tidal_exceptions.ObjectNotFound as err: - self.logger.warning(f"Failed to get toptracks for artist {prov_artist_id}: {err}") - return [] - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Get playlist tracks.""" - tidal_session = await self._get_tidal_session() - result: list[Track] = [] - page_size = 200 - offset = page * page_size - track_obj: TidalTrack # satisfy the type checker - tidal_tracks = await get_playlist_tracks( - tidal_session, prov_playlist_id, limit=page_size, offset=offset - ) - for index, track_obj in enumerate(tidal_tracks, 1): - track = self._parse_track(track_obj=track_obj) - track.position = offset + index - result.append(track) - return result - - @throttle_with_retries - async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]: - """Get similar tracks for given track id.""" - tidal_session = await self._get_tidal_session() - similar_tracks_obj = await get_similar_tracks(tidal_session, prov_track_id, limit) - return [self._parse_track(track) for track in similar_tracks_obj] - - async def library_add(self, item: MediaItemType) -> bool: - """Add item to library.""" - tidal_session = await self._get_tidal_session() - return await library_items_add_remove( - tidal_session, - str(self._tidal_user_id), - item.item_id, - item.media_type, - add=True, - ) - - async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: - """Remove item from library.""" - tidal_session = await self._get_tidal_session() - return await library_items_add_remove( - tidal_session, - str(self._tidal_user_id), - prov_item_id, - media_type, - add=False, - ) - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - tidal_session = await self._get_tidal_session() - return await add_playlist_tracks(tidal_session, prov_playlist_id, prov_track_ids) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - prov_track_ids = [] - tidal_session = await self._get_tidal_session() - for pos in positions_to_remove: - for tidal_track in await get_playlist_tracks( - tidal_session, prov_playlist_id, limit=1, offset=pos - 1 - ): - prov_track_ids.append(tidal_track.id) - return await remove_playlist_tracks(tidal_session, prov_playlist_id, prov_track_ids) - - async def create_playlist(self, name: str) -> Playlist: - """Create a new playlist on provider with given name.""" - tidal_session = await self._get_tidal_session() - playlist_obj = await create_playlist( - session=tidal_session, - user_id=str(self._tidal_user_id), - title=name, - description="", - ) - return self._parse_playlist(playlist_obj) - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - tidal_session = await self._get_tidal_session() - # make sure a valid track is requested. - if not (track := await get_track(tidal_session, item_id)): - msg = f"track {item_id} not found" - raise MediaNotFoundError(msg) - stream: TidalStream = await get_stream(track) - manifest = stream.get_stream_manifest() - if stream.is_mpd: - # for mpeg-dash streams we just pass the complete base64 manifest - url = f"data:application/dash+xml;base64,{manifest.manifest}" - else: - # as far as I can oversee a BTS stream is just a single URL - url = manifest.urls[0] - - return StreamDetails( - item_id=track.id, - provider=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(manifest.codecs), - sample_rate=manifest.sample_rate, - bit_depth=stream.bit_depth, - channels=2, - ), - stream_type=StreamType.HTTP, - duration=track.duration, - path=url, - ) - - @throttle_with_retries - async def get_artist(self, prov_artist_id: str) -> Artist: - """Get artist details for given artist id.""" - tidal_session = await self._get_tidal_session() - try: - artist_obj = await get_artist(tidal_session, prov_artist_id) - return self._parse_artist(artist_obj) - except tidal_exceptions.ObjectNotFound as err: - raise MediaNotFoundError from err - - @throttle_with_retries - async def get_album(self, prov_album_id: str) -> Album: - """Get album details for given album id.""" - tidal_session = await self._get_tidal_session() - try: - album_obj = await get_album(tidal_session, prov_album_id) - return self._parse_album(album_obj) - except tidal_exceptions.ObjectNotFound as err: - raise MediaNotFoundError from err - - @throttle_with_retries - async def get_track(self, prov_track_id: str) -> Track: - """Get track details for given track id.""" - tidal_session = await self._get_tidal_session() - track_obj = await get_track(tidal_session, prov_track_id) - try: - track = self._parse_track(track_obj) - # get some extra details for the full track info - with suppress(tidal_exceptions.MetadataNotAvailable, AttributeError): - lyrics: TidalLyrics = await asyncio.to_thread(track_obj.lyrics) - track.metadata.lyrics = lyrics.text - return track - except tidal_exceptions.ObjectNotFound as err: - raise MediaNotFoundError from err - - @throttle_with_retries - async def get_playlist(self, prov_playlist_id: str) -> Playlist: - """Get playlist details for given playlist id.""" - tidal_session = await self._get_tidal_session() - playlist_obj = await get_playlist(tidal_session, prov_playlist_id) - return self._parse_playlist(playlist_obj) - - def get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: - """Create a generic item mapping.""" - return ItemMapping( - media_type=media_type, - item_id=key, - provider=self.instance_id, - name=name, - ) - - async def _get_tidal_session(self) -> TidalSession: - """Ensure the current token is valid and return a tidal session.""" - if ( - self._tidal_session - and self._tidal_session.access_token - and datetime.fromisoformat(str(self.config.get_value(CONF_EXPIRY_TIME))) - > (datetime.now() + timedelta(days=1)) - ): - return self._tidal_session - self._tidal_session = await self._load_tidal_session( - token_type="Bearer", - quality=str(self.config.get_value(CONF_QUALITY)), - access_token=str(self.config.get_value(CONF_AUTH_TOKEN)), - refresh_token=str(self.config.get_value(CONF_REFRESH_TOKEN)), - expiry_time=datetime.fromisoformat(str(self.config.get_value(CONF_EXPIRY_TIME))), - ) - self.mass.config.set_raw_provider_config_value( - self.config.instance_id, - CONF_AUTH_TOKEN, - self._tidal_session.access_token, - encrypted=True, - ) - self.mass.config.set_raw_provider_config_value( - self.config.instance_id, - CONF_REFRESH_TOKEN, - self._tidal_session.refresh_token, - encrypted=True, - ) - self.mass.config.set_raw_provider_config_value( - self.config.instance_id, - CONF_EXPIRY_TIME, - self._tidal_session.expiry_time.isoformat(), - ) - return self._tidal_session - - async def _load_tidal_session( - self, - token_type: str, - quality: str, - access_token: str, - refresh_token: str, - expiry_time: datetime | None = None, - ) -> TidalSession: - """Load the tidalapi Session.""" - - def inner() -> TidalSession: - config = TidalConfig(quality=quality, item_limit=10000, alac=False) - session = TidalSession(config=config) - session.load_oauth_session( - token_type=token_type, - access_token=access_token, - refresh_token=refresh_token, - expiry_time=expiry_time, - is_pkce=True, - ) - return session - - return await asyncio.to_thread(inner) - - # Parsers - - def _parse_artist(self, artist_obj: TidalArtist) -> Artist: - """Parse tidal artist object to generic layout.""" - artist_id = artist_obj.id - artist = Artist( - item_id=str(artist_id), - provider=self.instance_id, - name=artist_obj.name, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - # NOTE: don't use the /browse endpoint as it's - # not working for musicbrainz lookups - url=f"https://tidal.com/artist/{artist_id}", - ) - }, - ) - # metadata - if artist_obj.picture: - picture_id = artist_obj.picture.replace("-", "/") - image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" - artist.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - - return artist - - def _parse_album(self, album_obj: TidalAlbum) -> Album: - """Parse tidal album object to generic layout.""" - name = album_obj.name - version = album_obj.version or "" - album_id = album_obj.id - album = Album( - item_id=str(album_id), - provider=self.instance_id, - name=name, - version=version, - provider_mappings={ - ProviderMapping( - item_id=str(album_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.FLAC, - ), - url=f"https://tidal.com/album/{album_id}", - available=album_obj.available, - ) - }, - ) - various_artist_album: bool = False - for artist_obj in album_obj.artists: - if artist_obj.name == "Various Artists": - various_artist_album = True - album.artists.append(self._parse_artist(artist_obj)) - - if album_obj.type == "COMPILATION" or various_artist_album: - album.album_type = AlbumType.COMPILATION - elif album_obj.type == "ALBUM": - album.album_type = AlbumType.ALBUM - elif album_obj.type == "EP": - album.album_type = AlbumType.EP - elif album_obj.type == "SINGLE": - album.album_type = AlbumType.SINGLE - - album.year = int(album_obj.year) - # metadata - if album_obj.universal_product_number: - album.external_ids.add((ExternalID.BARCODE, album_obj.universal_product_number)) - album.metadata.copyright = album_obj.copyright - album.metadata.explicit = album_obj.explicit - album.metadata.popularity = album_obj.popularity - if album_obj.cover: - picture_id = album_obj.cover.replace("-", "/") - image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" - album.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - - return album - - def _parse_track( - self, - track_obj: TidalTrack, - ) -> Track: - """Parse tidal track object to generic layout.""" - version = track_obj.version or "" - track_id = str(track_obj.id) - track = Track( - item_id=str(track_id), - provider=self.instance_id, - name=track_obj.name, - version=version, - duration=track_obj.duration, - provider_mappings={ - ProviderMapping( - item_id=str(track_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.FLAC, - bit_depth=24 if track_obj.is_hi_res_lossless else 16, - ), - url=f"https://tidal.com/track/{track_id}", - available=track_obj.available, - ) - }, - disc_number=track_obj.volume_num or 0, - track_number=track_obj.track_num or 0, - ) - if track_obj.isrc: - track.external_ids.add((ExternalID.ISRC, track_obj.isrc)) - track.artists = UniqueList() - for track_artist in track_obj.artists: - artist = self._parse_artist(track_artist) - track.artists.append(artist) - # metadata - track.metadata.explicit = track_obj.explicit - track.metadata.popularity = track_obj.popularity - track.metadata.copyright = track_obj.copyright - if track_obj.album: - # Here we use an ItemMapping as Tidal returns - # minimal data when getting an Album from a Track - track.album = self.get_item_mapping( - media_type=MediaType.ALBUM, - key=str(track_obj.album.id), - name=track_obj.album.name, - ) - if track_obj.album.cover: - picture_id = track_obj.album.cover.replace("-", "/") - image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" - track.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - return track - - def _parse_playlist(self, playlist_obj: TidalPlaylist) -> Playlist: - """Parse tidal playlist object to generic layout.""" - playlist_id = playlist_obj.id - creator_id = playlist_obj.creator.id if playlist_obj.creator else None - creator_name = playlist_obj.creator.name if playlist_obj.creator else "Tidal" - playlist = Playlist( - item_id=str(playlist_id), - provider=self.instance_id, - name=playlist_obj.name, - owner=creator_name, - provider_mappings={ - ProviderMapping( - item_id=str(playlist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f"{BROWSE_URL}/playlist/{playlist_id}", - ) - }, - ) - is_editable = bool(creator_id and str(creator_id) == self._tidal_user_id) - playlist.is_editable = is_editable - # metadata - playlist.cache_checksum = str(playlist_obj.last_updated) - playlist.metadata.popularity = playlist_obj.popularity - if picture := (playlist_obj.square_picture or playlist_obj.picture): - picture_id = picture.replace("-", "/") - image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" - playlist.metadata.images = UniqueList( - [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - ) - - return playlist - - async def _iter_items( - self, - func: Callable[_P, list[_R]] | Callable[_P, Awaitable[list[_R]]], - *args: _P.args, - **kwargs: _P.kwargs, - ) -> AsyncGenerator[_R, None]: - """Yield all items from a larger listing.""" - offset = 0 - while True: - if asyncio.iscoroutinefunction(func): - chunk = await func(*args, **kwargs, offset=offset) # type: ignore[arg-type] - else: - chunk = await asyncio.to_thread(func, *args, **kwargs, offset=offset) # type: ignore[arg-type] - offset += len(chunk) - for item in chunk: - yield item - if len(chunk) < DEFAULT_LIMIT: - break - - async def _get_media_info( - self, item_id: str, url: str, force_refresh: bool = False - ) -> AudioTags: - """Retrieve (cached) mediainfo for track.""" - cache_category = CacheCategory.MEDIA_INFO - cache_base_key = self.lookup_key - # do we have some cached info for this url ? - cached_info = await self.mass.cache.get( - item_id, category=cache_category, base_key=cache_base_key - ) - if cached_info and not force_refresh: - media_info = AudioTags.parse(cached_info) - else: - # parse info with ffprobe (and store in cache) - media_info = await parse_tags(url) - await self.mass.cache.set( - item_id, - media_info.raw, - category=cache_category, - base_key=cache_base_key, - ) - return media_info diff --git a/music_assistant/server/providers/tidal/helpers.py b/music_assistant/server/providers/tidal/helpers.py deleted file mode 100644 index f443133f..00000000 --- a/music_assistant/server/providers/tidal/helpers.py +++ /dev/null @@ -1,389 +0,0 @@ -"""Helper module for parsing the Tidal API. - -This helpers file is an async wrapper around the excellent tidalapi package. -While the tidalapi package does an excellent job at parsing the Tidal results, -it is unfortunately not async, which is required for Music Assistant to run smoothly. -This also nicely separates the parsing logic from the Tidal provider logic. - -CREDITS: -tidalapi: https://github.com/tamland/python-tidal -""" - -import asyncio -import logging - -from tidalapi import Album as TidalAlbum -from tidalapi import Artist as TidalArtist -from tidalapi import Favorites as TidalFavorites -from tidalapi import LoggedInUser -from tidalapi import Playlist as TidalPlaylist -from tidalapi import Session as TidalSession -from tidalapi import Track as TidalTrack -from tidalapi import UserPlaylist as TidalUserPlaylist -from tidalapi.exceptions import MetadataNotAvailable, ObjectNotFound, TooManyRequests -from tidalapi.media import Stream as TidalStream - -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable - -DEFAULT_LIMIT = 50 -LOGGER = logging.getLogger(__name__) - - -async def get_library_artists( - session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 -) -> list[TidalArtist]: - """Async wrapper around the tidalapi Favorites.artists function.""" - - def inner() -> list[TidalArtist]: - artists: list[TidalArtist] = TidalFavorites(session, user_id).artists( - limit=limit, offset=offset - ) - return artists - - return await asyncio.to_thread(inner) - - -async def library_items_add_remove( - session: TidalSession, - user_id: str, - item_id: str, - media_type: MediaType, - add: bool = True, -) -> bool: - """Async wrapper around the tidalapi Favorites.items add/remove function.""" - - def inner() -> bool: - tidal_favorites = TidalFavorites(session, user_id) - if media_type == MediaType.UNKNOWN: - return False - response: bool = False - if add: - match media_type: - case MediaType.ARTIST: - response = tidal_favorites.add_artist(item_id) - case MediaType.ALBUM: - response = tidal_favorites.add_album(item_id) - case MediaType.TRACK: - response = tidal_favorites.add_track(item_id) - case MediaType.PLAYLIST: - response = tidal_favorites.add_playlist(item_id) - else: - match media_type: - case MediaType.ARTIST: - response = tidal_favorites.remove_artist(item_id) - case MediaType.ALBUM: - response = tidal_favorites.remove_album(item_id) - case MediaType.TRACK: - response = tidal_favorites.remove_track(item_id) - case MediaType.PLAYLIST: - response = tidal_favorites.remove_playlist(item_id) - return response - - return await asyncio.to_thread(inner) - - -async def get_artist(session: TidalSession, prov_artist_id: str) -> TidalArtist: - """Async wrapper around the tidalapi Artist function.""" - - def inner() -> TidalArtist: - try: - return TidalArtist(session, prov_artist_id) - except ObjectNotFound as err: - msg = f"Artist {prov_artist_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_artist_albums(session: TidalSession, prov_artist_id: str) -> list[TidalAlbum]: - """Async wrapper around 3 tidalapi album functions.""" - - def inner() -> list[TidalAlbum]: - try: - artist_obj = TidalArtist(session, prov_artist_id) - except ObjectNotFound as err: - msg = f"Artist {prov_artist_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - else: - all_albums: list[TidalAlbum] = artist_obj.get_albums(limit=DEFAULT_LIMIT) - # extend with EPs and singles - all_albums.extend(artist_obj.get_ep_singles(limit=DEFAULT_LIMIT)) - # extend with compilations - # note that the Tidal API gives back really strange results here so - # filter on either various artists or the artist id - for album in artist_obj.get_other(limit=DEFAULT_LIMIT): - if album.artist.id == artist_obj.id or album.artist.name == "Various Artists": - all_albums.append(album) - return all_albums - - return await asyncio.to_thread(inner) - - -async def get_artist_toptracks( - session: TidalSession, prov_artist_id: str, limit: int = 10, offset: int = 0 -) -> list[TidalTrack]: - """Async wrapper around the tidalapi Artist.get_top_tracks function.""" - - def inner() -> list[TidalTrack]: - top_tracks: list[TidalTrack] = TidalArtist(session, prov_artist_id).get_top_tracks( - limit=limit, offset=offset - ) - return top_tracks - - return await asyncio.to_thread(inner) - - -async def get_library_albums( - session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 -) -> list[TidalAlbum]: - """Async wrapper around the tidalapi Favorites.albums function.""" - - def inner() -> list[TidalAlbum]: - albums: list[TidalAlbum] = TidalFavorites(session, user_id).albums( - limit=limit, offset=offset - ) - return albums - - return await asyncio.to_thread(inner) - - -async def get_album(session: TidalSession, prov_album_id: str) -> TidalAlbum: - """Async wrapper around the tidalapi Album function.""" - - def inner() -> TidalAlbum: - try: - return TidalAlbum(session, prov_album_id) - except ObjectNotFound as err: - msg = f"Album {prov_album_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_track(session: TidalSession, prov_track_id: str) -> TidalTrack: - """Async wrapper around the tidalapi Track function.""" - - def inner() -> TidalTrack: - try: - return TidalTrack(session, prov_track_id) - except ObjectNotFound as err: - msg = f"Track {prov_track_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_stream(track: TidalTrack) -> TidalStream: - """Async wrapper around the tidalapi Track.get_stream_url function.""" - - def inner() -> TidalStream: - try: - return track.get_stream() - except ObjectNotFound as err: - msg = f"Track {track.id} has no available stream" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_track_url(session: TidalSession, prov_track_id: str) -> str: - """Async wrapper around the tidalapi Track.get_url function.""" - - def inner() -> str: - try: - track_url: str = TidalTrack(session, prov_track_id).get_url() - return track_url - except ObjectNotFound as err: - msg = f"Track {prov_track_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_album_tracks(session: TidalSession, prov_album_id: str) -> list[TidalTrack]: - """Async wrapper around the tidalapi Album.tracks function.""" - - def inner() -> list[TidalTrack]: - try: - tracks: list[TidalTrack] = TidalAlbum(session, prov_album_id).tracks( - limit=DEFAULT_LIMIT - ) - return tracks - except ObjectNotFound as err: - msg = f"Album {prov_album_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_library_tracks( - session: TidalSession, user_id: str, limit: int = DEFAULT_LIMIT, offset: int = 0 -) -> list[TidalTrack]: - """Async wrapper around the tidalapi Favorites.tracks function.""" - - def inner() -> list[TidalTrack]: - tracks: list[TidalTrack] = TidalFavorites(session, user_id).tracks( - limit=limit, offset=offset - ) - return tracks - - return await asyncio.to_thread(inner) - - -async def get_library_playlists( - session: TidalSession, user_id: str, offset: int = 0 -) -> list[TidalPlaylist]: - """Async wrapper around the tidalapi LoggedInUser.playlist_and_favorite_playlists function.""" - - def inner() -> list[TidalPlaylist]: - playlists: list[TidalPlaylist] = LoggedInUser( - session, user_id - ).playlist_and_favorite_playlists(offset=offset) - return playlists - - return await asyncio.to_thread(inner) - - -async def get_playlist(session: TidalSession, prov_playlist_id: str) -> TidalPlaylist: - """Async wrapper around the tidal Playlist function.""" - - def inner() -> TidalPlaylist: - try: - return TidalPlaylist(session, prov_playlist_id) - except ObjectNotFound as err: - msg = f"Playlist {prov_playlist_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def get_playlist_tracks( - session: TidalSession, - prov_playlist_id: str, - limit: int = DEFAULT_LIMIT, - offset: int = 0, -) -> list[TidalTrack]: - """Async wrapper around the tidal Playlist.tracks function.""" - - def inner() -> list[TidalTrack]: - try: - tracks: list[TidalTrack] = TidalPlaylist(session, prov_playlist_id).tracks( - limit=limit, offset=offset - ) - return tracks - except ObjectNotFound as err: - msg = f"Playlist {prov_playlist_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def add_playlist_tracks( - session: TidalSession, prov_playlist_id: str, track_ids: list[str] -) -> None: - """Async wrapper around the tidal Playlist.add function.""" - - def inner() -> None: - TidalUserPlaylist(session, prov_playlist_id).add(track_ids) - - return await asyncio.to_thread(inner) - - -async def remove_playlist_tracks( - session: TidalSession, prov_playlist_id: str, track_ids: list[str] -) -> None: - """Async wrapper around the tidal Playlist.remove function.""" - - def inner() -> None: - for item in track_ids: - TidalUserPlaylist(session, prov_playlist_id).remove_by_id(int(item)) - - return await asyncio.to_thread(inner) - - -async def create_playlist( - session: TidalSession, user_id: str, title: str, description: str | None = None -) -> TidalPlaylist: - """Async wrapper around the tidal LoggedInUser.create_playlist function.""" - - def inner() -> TidalPlaylist: - playlist: TidalPlaylist = LoggedInUser(session, user_id).create_playlist(title, description) - return playlist - - return await asyncio.to_thread(inner) - - -async def get_similar_tracks( - session: TidalSession, prov_track_id: str, limit: int = 25 -) -> list[TidalTrack]: - """Async wrapper around the tidal Track.get_similar_tracks function.""" - - def inner() -> list[TidalTrack]: - try: - tracks: list[TidalTrack] = TidalTrack(session, prov_track_id).get_track_radio( - limit=limit - ) - return tracks - except (MetadataNotAvailable, ObjectNotFound) as err: - msg = f"Track {prov_track_id} not found" - raise MediaNotFoundError(msg) from err - except TooManyRequests: - msg = "Tidal API rate limit reached" - raise ResourceTemporarilyUnavailable(msg) - - return await asyncio.to_thread(inner) - - -async def search( - session: TidalSession, - query: str, - media_types: list[MediaType], - limit: int = 50, - offset: int = 0, -) -> dict[str, str]: - """Async wrapper around the tidalapi Search function.""" - - def inner() -> dict[str, str]: - search_types = [] - if MediaType.ARTIST in media_types: - search_types.append(TidalArtist) - if MediaType.ALBUM in media_types: - search_types.append(TidalAlbum) - if MediaType.TRACK in media_types: - search_types.append(TidalTrack) - if MediaType.PLAYLIST in media_types: - search_types.append(TidalPlaylist) - - models = search_types - results: dict[str, str] = session.search(query, models, limit, offset) - return results - - return await asyncio.to_thread(inner) diff --git a/music_assistant/server/providers/tidal/icon.svg b/music_assistant/server/providers/tidal/icon.svg deleted file mode 100644 index 8bf8e023..00000000 --- a/music_assistant/server/providers/tidal/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/music_assistant/server/providers/tidal/icon_dark.svg b/music_assistant/server/providers/tidal/icon_dark.svg deleted file mode 100644 index 889198f7..00000000 --- a/music_assistant/server/providers/tidal/icon_dark.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/music_assistant/server/providers/tidal/manifest.json b/music_assistant/server/providers/tidal/manifest.json deleted file mode 100644 index d21ddd5a..00000000 --- a/music_assistant/server/providers/tidal/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "tidal", - "name": "Tidal", - "description": "Support for the Tidal streaming provider in Music Assistant.", - "codeowners": ["@jozefKruszynski"], - "requirements": ["tidalapi==0.8.0"], - "documentation": "https://music-assistant.io/music-providers/tidal/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/server/providers/tunein/__init__.py deleted file mode 100644 index 55265b1b..00000000 --- a/music_assistant/server/providers/tunein/__init__.py +++ /dev/null @@ -1,293 +0,0 @@ -"""Tune-In musicprovider support for MusicAssistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import InvalidDataError, LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( - AudioFormat, - ContentType, - ImageType, - MediaItemImage, - MediaType, - ProviderMapping, - Radio, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.constants import CONF_USERNAME -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.music_provider import MusicProvider - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_RADIOS, - ProviderFeature.BROWSE, -) - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - if not config.get_value(CONF_USERNAME): - msg = "Username is invalid" - raise LoginFailed(msg) - - return TuneInProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - # ruff: noqa: ARG001 - return ( - ConfigEntry( - key=CONF_USERNAME, - type=ConfigEntryType.STRING, - label="Username", - required=True, - ), - ) - - -class TuneInProvider(MusicProvider): - """Provider implementation for Tune In.""" - - _throttler: Throttler - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def handle_async_init(self) -> None: - """Handle async initialization of the provider.""" - self._throttler = Throttler(rate_limit=1, period=2) - if "@" in self.config.get_value(CONF_USERNAME): - self.logger.warning( - "Email address detected instead of username, " - "it is advised to use the tunein username instead of email." - ) - - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: - """Retrieve library/subscribed radio stations from the provider.""" - - async def parse_items( - items: list[dict], folder: str | None = None - ) -> AsyncGenerator[Radio, None]: - for item in items: - item_type = item.get("type", "") - if "unavailable" in item.get("key", ""): - continue - if not item.get("is_available", True): - continue - if item_type == "audio": - if "preset_id" not in item: - continue - # each radio station can have multiple streams add each one as different quality - stream_info = await self._get_stream_info(item["preset_id"]) - yield self._parse_radio(item, stream_info, folder) - elif item_type == "link" and item.get("item") == "url": - # custom url - try: - yield self._parse_radio(item) - except InvalidDataError as err: - # there may be invalid custom urls, ignore those - self.logger.warning(str(err)) - elif item_type == "link": - # stations are in sublevel (new style) - if sublevel := await self.__get_data(item["URL"], render="json"): - async for subitem in parse_items(sublevel["body"], item["text"]): - yield subitem - elif item.get("children"): - # stations are in sublevel (old style ?) - async for subitem in parse_items(item["children"], item["text"]): - yield subitem - - data = await self.__get_data("Browse.ashx", c="presets") - if data and "body" in data: - async for item in parse_items(data["body"]): - yield item - - async def get_radio(self, prov_radio_id: str) -> Radio: - """Get radio station details.""" - if not prov_radio_id.startswith("http"): - if "--" in prov_radio_id: - prov_radio_id, media_type = prov_radio_id.split("--", 1) - else: - media_type = None - params = {"c": "composite", "detail": "listing", "id": prov_radio_id} - result = await self.__get_data("Describe.ashx", **params) - if result and result.get("body") and result["body"][0].get("children"): - item = result["body"][0]["children"][0] - stream_info = await self._get_stream_info(prov_radio_id) - for stream in stream_info: - if media_type and stream["media_type"] != media_type: - continue - return self._parse_radio(item, [stream]) - # fallback - e.g. for handle custom urls ... - async for radio in self.get_library_radios(): - if radio.item_id == prov_radio_id: - return radio - msg = f"Item {prov_radio_id} not found" - raise MediaNotFoundError(msg) - - def _parse_radio( - self, details: dict, stream_info: list[dict] | None = None, folder: str | None = None - ) -> Radio: - """Parse Radio object from json obj returned from api.""" - if "name" in details: - name = details["name"] - else: - # parse name from text attr - name = details["text"] - if " | " in name: - name = name.split(" | ")[1] - name = name.split(" (")[0] - - if stream_info is not None: - # stream info is provided: parse stream objects into provider mappings - radio = Radio( - item_id=details["preset_id"], - provider=self.lookup_key, - name=name, - provider_mappings={ - ProviderMapping( - item_id=f'{details["preset_id"]}--{stream["media_type"]}', - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(stream["media_type"]), - bit_rate=stream.get("bitrate", 128), - ), - details=stream["url"], - available=details.get("is_available", True), - ) - for stream in stream_info - }, - ) - else: - # custom url (no stream object present) - radio = Radio( - item_id=details["URL"], - provider=self.lookup_key, - name=name, - provider_mappings={ - ProviderMapping( - item_id=details["URL"], - provider_domain=self.domain, - provider_instance=self.instance_id, - audio_format=AudioFormat( - content_type=ContentType.UNKNOWN, - ), - details=details["URL"], - available=details.get("is_available", True), - ) - }, - ) - - # preset number is used for sorting (not present at stream time) - preset_number = details.get("preset_number", 0) - radio.position = preset_number - if "text" in details: - radio.metadata.description = details["text"] - # image - if img := details.get("image") or details.get("logo"): - radio.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=img, - provider=self.lookup_key, - remotely_accessible=True, - ) - ] - return radio - - async def _get_stream_info(self, preset_id: str) -> list[dict]: - """Get stream info for a radio station.""" - cache_base_key = "tunein_stream" - if cache := await self.mass.cache.get(preset_id, base_key=cache_base_key): - return cache - result = (await self.__get_data("Tune.ashx", id=preset_id))["body"] - await self.mass.cache.set(preset_id, result, base_key=cache_base_key) - return result - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Get streamdetails for a radio station.""" - if item_id.startswith("http"): - # custom url - return StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=AudioFormat( - content_type=ContentType.UNKNOWN, - ), - media_type=MediaType.RADIO, - stream_type=StreamType.HTTP, - path=item_id, - can_seek=False, - ) - if "--" in item_id: - stream_item_id, media_type = item_id.split("--", 1) - else: - media_type = None - stream_item_id = item_id - for stream in await self._get_stream_info(stream_item_id): - if media_type and stream["media_type"] != media_type: - continue - return StreamDetails( - provider=self.domain, - item_id=item_id, - # set contenttype to unknown so ffmpeg can auto detect it - audio_format=AudioFormat(content_type=ContentType.UNKNOWN), - media_type=MediaType.RADIO, - stream_type=StreamType.HTTP, - path=stream["url"], - can_seek=False, - ) - msg = f"Unable to retrieve stream details for {item_id}" - raise MediaNotFoundError(msg) - - async def __get_data(self, endpoint: str, **kwargs): - """Get data from api.""" - if endpoint.startswith("http"): - url = endpoint - else: - url = f"https://opml.radiotime.com/{endpoint}" - kwargs["formats"] = "ogg,aac,wma,mp3,hls" - kwargs["username"] = self.config.get_value(CONF_USERNAME) - kwargs["partnerId"] = "1" - kwargs["render"] = "json" - locale = self.mass.metadata.locale.replace("_", "-") - language = locale.split("-")[0] - headers = {"Accept-Language": f"{locale}, {language};q=0.9, *;q=0.5"} - async with ( - self._throttler, - self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, - ): - result = await response.json() - if not result or "error" in result: - self.logger.error(url) - self.logger.error(kwargs) - result = None - return result diff --git a/music_assistant/server/providers/tunein/icon.svg b/music_assistant/server/providers/tunein/icon.svg deleted file mode 100644 index 55cc9fee..00000000 --- a/music_assistant/server/providers/tunein/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/music_assistant/server/providers/tunein/manifest.json b/music_assistant/server/providers/tunein/manifest.json deleted file mode 100644 index 57429700..00000000 --- a/music_assistant/server/providers/tunein/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "tunein", - "name": "Tune-In Radio", - "description": "Play your favorite radio stations from Tune-In in Music Assistant.", - "codeowners": ["@music-assistant"], - "requirements": [], - "documentation": "https://music-assistant.io/music-providers/tunein/", - "multi_instance": true -} diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py deleted file mode 100644 index 34f1f0f8..00000000 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ /dev/null @@ -1,859 +0,0 @@ -"""Youtube Music support for MusicAssistant.""" - -from __future__ import annotations - -import asyncio -import logging -from collections.abc import AsyncGenerator -from time import time -from typing import TYPE_CHECKING, Any -from urllib.parse import unquote - -import yt_dlp -from ytmusicapi.constants import SUPPORTED_LANGUAGES -from ytmusicapi.exceptions import YTMusicServerError - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import ( - InvalidDataError, - LoginFailed, - MediaNotFoundError, - UnplayableMediaError, -) -from music_assistant.common.models.media_items import ( - Album, - AlbumType, - Artist, - AudioFormat, - ContentType, - ImageType, - ItemMapping, - MediaItemImage, - MediaItemType, - MediaType, - Playlist, - ProviderMapping, - SearchResults, - Track, -) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models.music_provider import MusicProvider - -from .helpers import ( - add_remove_playlist_tracks, - get_album, - get_artist, - get_library_albums, - get_library_artists, - get_library_playlists, - get_library_tracks, - get_playlist, - get_song_radio_tracks, - get_track, - library_add_remove_album, - library_add_remove_artist, - library_add_remove_playlist, - login_oauth, - refresh_oauth_token, - search, -) - -if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - - -CONF_COOKIE = "cookie" -CONF_ACTION_AUTH = "auth" -CONF_AUTH_TOKEN = "auth_token" -CONF_REFRESH_TOKEN = "refresh_token" -CONF_TOKEN_TYPE = "token_type" -CONF_EXPIRY_TIME = "expiry_time" - -YTM_DOMAIN = "https://music.youtube.com" -YTM_BASE_URL = f"{YTM_DOMAIN}/youtubei/v1/" -VARIOUS_ARTISTS_YTM_ID = "UCUTXlgdcKU5vfzFqHOWIvkA" -# Playlist ID's are not unique across instances for lists like 'Liked videos', 'SuperMix' etc. -# So we need to add a delimiter to make them unique -YT_PLAYLIST_ID_DELIMITER = "🎵" -YT_PERSONAL_PLAYLISTS = ( - "LM", # Liked songs - "SE" # Episodes for Later - "RDTMAK5uy_kset8DisdE7LSD4TNjEVvrKRTmG7a56sY", # SuperMix - "RDTMAK5uy_nGQKSMIkpr4o9VI_2i56pkGliD6FQRo50", # My Mix 1 - "RDTMAK5uy_lz2owBgwWf1mjzyn_NbxzMViQzIg8IAIg", # My Mix 2 - "RDTMAK5uy_k5UUl0lmrrfrjMpsT0CoMpdcBz1ruAO1k", # My Mix 3 - "RDTMAK5uy_nTsa0Irmcu2li2-qHBoZxtrpG9HuC3k_Q", # My Mix 4 - "RDTMAK5uy_lfZhS7zmIcmUhsKtkWylKzc0EN0LW90-s", # My Mix 5 - "RDTMAK5uy_k78ni6Y4fyyl0r2eiKkBEICh9Q5wJdfXk", # My Mix 6 - "RDTMAK5uy_lfhhWWw9v71CPrR7MRMHgZzbH6Vku9iJc", # My Mix 7 - "RDTMAK5uy_n_5IN6hzAOwdCnM8D8rzrs3vDl12UcZpA", # Discover Mix - "RDTMAK5uy_lr0LWzGrq6FU9GIxWvFHTRPQD2LHMqlFA", # New Release Mix - "RDTMAK5uy_nilrsVWxrKskY0ZUpVZ3zpB0u4LwWTVJ4", # Replay Mix - "RDTMAK5uy_mZtXeU08kxXJOUhL0ETdAuZTh1z7aAFAo", # Archive Mix -) -YTM_PREMIUM_CHECK_TRACK_ID = "dQw4w9WgXcQ" - -SUPPORTED_FEATURES = ( - ProviderFeature.LIBRARY_ARTISTS, - ProviderFeature.LIBRARY_ALBUMS, - ProviderFeature.LIBRARY_TRACKS, - ProviderFeature.LIBRARY_PLAYLISTS, - ProviderFeature.BROWSE, - ProviderFeature.SEARCH, - ProviderFeature.ARTIST_ALBUMS, - ProviderFeature.ARTIST_TOPTRACKS, - ProviderFeature.SIMILAR_TRACKS, -) - - -# TODO: fix disabled tests -# ruff: noqa: PLW2901, RET504 - - -async def setup( - mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig -) -> ProviderInstanceType: - """Initialize provider(instance) with given configuration.""" - return YoutubeMusicProvider(mass, manifest, config) - - -async def get_config_entries( - mass: MusicAssistant, - instance_id: str | None = None, # noqa: ARG001 - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, -) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup this provider. - - instance_id: id of an existing provider instance (None if new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - if action == CONF_ACTION_AUTH: - async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: - token = await login_oauth(auth_helper) - values[CONF_AUTH_TOKEN] = token["access_token"] - values[CONF_REFRESH_TOKEN] = token["refresh_token"] - values[CONF_EXPIRY_TIME] = token["expires_in"] - values[CONF_TOKEN_TYPE] = token["token_type"] - # return the collected config entries - return ( - ConfigEntry( - key=CONF_AUTH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label="Authentication token for Youtube Music", - description="You need to link Music Assistant to your Youtube Music account. " - "Please ignore the code on the page the next page and click 'Next'.", - action=CONF_ACTION_AUTH, - action_label="Authenticate on Youtube Music", - value=values.get(CONF_AUTH_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_REFRESH_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label=CONF_REFRESH_TOKEN, - hidden=True, - value=values.get(CONF_REFRESH_TOKEN) if values else None, - ), - ConfigEntry( - key=CONF_EXPIRY_TIME, - type=ConfigEntryType.INTEGER, - label="Expiry time of auth token for Youtube Music", - hidden=True, - value=values.get(CONF_EXPIRY_TIME) if values else None, - ), - ConfigEntry( - key=CONF_TOKEN_TYPE, - type=ConfigEntryType.STRING, - label="The token type required to create headers", - hidden=True, - value=values.get(CONF_TOKEN_TYPE) if values else None, - ), - ) - - -class YoutubeMusicProvider(MusicProvider): - """Provider for Youtube Music.""" - - _headers = None - _context = None - _cookies = None - _cipher = None - - async def handle_async_init(self) -> None: - """Set up the YTMusic provider.""" - logging.getLogger("yt_dlp").setLevel(self.logger.level + 10) - if not self.config.get_value(CONF_AUTH_TOKEN): - msg = "Invalid login credentials" - raise LoginFailed(msg) - self._initialize_headers() - self._initialize_context() - self._cookies = {"CONSENT": "YES+1"} - # get default language (that is supported by YTM) - mass_locale = self.mass.metadata.locale - for lang_code in SUPPORTED_LANGUAGES: - if lang_code in (mass_locale, mass_locale.split("_")[0]): - self.language = lang_code - break - else: - self.language = "en" - if not await self._user_has_ytm_premium(): - raise LoginFailed("User does not have Youtube Music Premium") - - @property - def supported_features(self) -> tuple[ProviderFeature, ...]: - """Return the features supported by this Provider.""" - return SUPPORTED_FEATURES - - async def search( - self, search_query: str, media_types=list[MediaType], limit: int = 5 - ) -> SearchResults: - """Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - parsed_results = SearchResults() - ytm_filter = None - if len(media_types) == 1: - # YTM does not support multiple searchtypes, falls back to all if no type given - if media_types[0] == MediaType.ARTIST: - ytm_filter = "artists" - if media_types[0] == MediaType.ALBUM: - ytm_filter = "albums" - if media_types[0] == MediaType.TRACK: - ytm_filter = "songs" - if media_types[0] == MediaType.PLAYLIST: - ytm_filter = "playlists" - if media_types[0] == MediaType.RADIO: - # bit of an edge case but still good to handle - return parsed_results - results = await search( - query=search_query, ytm_filter=ytm_filter, limit=limit, language=self.language - ) - parsed_results = SearchResults() - for result in results: - try: - if result["resultType"] == "artist" and MediaType.ARTIST in media_types: - parsed_results.artists.append(self._parse_artist(result)) - elif result["resultType"] == "album" and MediaType.ALBUM in media_types: - parsed_results.albums.append(self._parse_album(result)) - elif result["resultType"] == "playlist" and MediaType.PLAYLIST in media_types: - parsed_results.playlists.append(self._parse_playlist(result)) - elif ( - result["resultType"] in ("song", "video") - and MediaType.TRACK in media_types - and (track := self._parse_track(result)) - ): - parsed_results.tracks.append(track) - except InvalidDataError: - pass # ignore invalid item - return parsed_results - - async def get_library_artists(self) -> AsyncGenerator[Artist, None]: - """Retrieve all library artists from Youtube Music.""" - await self._check_oauth_token() - artists_obj = await get_library_artists(headers=self._headers, language=self.language) - for artist in artists_obj: - yield self._parse_artist(artist) - - async def get_library_albums(self) -> AsyncGenerator[Album, None]: - """Retrieve all library albums from Youtube Music.""" - await self._check_oauth_token() - albums_obj = await get_library_albums(headers=self._headers, language=self.language) - for album in albums_obj: - yield self._parse_album(album, album["browseId"]) - - async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: - """Retrieve all library playlists from the provider.""" - await self._check_oauth_token() - playlists_obj = await get_library_playlists(headers=self._headers, language=self.language) - for playlist in playlists_obj: - yield self._parse_playlist(playlist) - - async def get_library_tracks(self) -> AsyncGenerator[Track, None]: - """Retrieve library tracks from Youtube Music.""" - await self._check_oauth_token() - tracks_obj = await get_library_tracks(headers=self._headers, language=self.language) - for track in tracks_obj: - # Library tracks sometimes do not have a valid artist id - # In that case, call the API for track details based on track id - try: - yield self._parse_track(track) - except InvalidDataError: - track = await self.get_track(track["videoId"]) - yield track - - async def get_album(self, prov_album_id) -> Album: - """Get full album details by id.""" - await self._check_oauth_token() - if album_obj := await get_album(prov_album_id=prov_album_id, language=self.language): - return self._parse_album(album_obj=album_obj, album_id=prov_album_id) - msg = f"Item {prov_album_id} not found" - raise MediaNotFoundError(msg) - - async def get_album_tracks(self, prov_album_id: str) -> list[Track]: - """Get album tracks for given album id.""" - await self._check_oauth_token() - album_obj = await get_album(prov_album_id=prov_album_id, language=self.language) - if not album_obj.get("tracks"): - return [] - tracks = [] - for track_obj in album_obj["tracks"]: - try: - track = self._parse_track(track_obj=track_obj) - except InvalidDataError: - continue - tracks.append(track) - return tracks - - async def get_artist(self, prov_artist_id) -> Artist: - """Get full artist details by id.""" - await self._check_oauth_token() - if artist_obj := await get_artist( - prov_artist_id=prov_artist_id, headers=self._headers, language=self.language - ): - return self._parse_artist(artist_obj=artist_obj) - msg = f"Item {prov_artist_id} not found" - raise MediaNotFoundError(msg) - - async def get_track(self, prov_track_id) -> Track: - """Get full track details by id.""" - await self._check_oauth_token() - if track_obj := await get_track( - prov_track_id=prov_track_id, - headers=self._headers, - language=self.language, - ): - return self._parse_track(track_obj) - msg = f"Item {prov_track_id} not found" - raise MediaNotFoundError(msg) - - async def get_playlist(self, prov_playlist_id) -> Playlist: - """Get full playlist details by id.""" - await self._check_oauth_token() - # Grab the playlist id from the full url in case of personal playlists - if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: - prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] - if playlist_obj := await get_playlist( - prov_playlist_id=prov_playlist_id, headers=self._headers, language=self.language - ): - return self._parse_playlist(playlist_obj) - msg = f"Item {prov_playlist_id} not found" - raise MediaNotFoundError(msg) - - async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: - """Return playlist tracks for the given provider playlist id.""" - if page > 0: - # paging not supported, we always return the whole list at once - return [] - await self._check_oauth_token() - # Grab the playlist id from the full url in case of personal playlists - if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: - prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] - # Add a try to prevent MA from stopping syncing whenever we fail a single playlist - try: - playlist_obj = await get_playlist( - prov_playlist_id=prov_playlist_id, headers=self._headers - ) - except KeyError as ke: - self.logger.warning("Could not load playlist: %s: %s", prov_playlist_id, ke) - return [] - if "tracks" not in playlist_obj: - return [] - result = [] - # TODO: figure out how to handle paging in YTM - for index, track_obj in enumerate(playlist_obj["tracks"], 1): - if track_obj["isAvailable"]: - # Playlist tracks sometimes do not have a valid artist id - # In that case, call the API for track details based on track id - try: - if track := self._parse_track(track_obj): - track.position = index - result.append(track) - except InvalidDataError: - if track := await self.get_track(track_obj["videoId"]): - track.position = index - result.append(track) - # YTM doesn't seem to support paging so we ignore offset and limit - return result - - async def get_artist_albums(self, prov_artist_id) -> list[Album]: - """Get a list of albums for the given artist.""" - await self._check_oauth_token() - artist_obj = await get_artist(prov_artist_id=prov_artist_id, headers=self._headers) - if "albums" in artist_obj and "results" in artist_obj["albums"]: - albums = [] - for album_obj in artist_obj["albums"]["results"]: - if "artists" not in album_obj: - album_obj["artists"] = [ - {"id": artist_obj["channelId"], "name": artist_obj["name"]} - ] - albums.append(self._parse_album(album_obj, album_obj["browseId"])) - return albums - return [] - - async def get_artist_toptracks(self, prov_artist_id) -> list[Track]: - """Get a list of 25 most popular tracks for the given artist.""" - await self._check_oauth_token() - artist_obj = await get_artist(prov_artist_id=prov_artist_id, headers=self._headers) - if artist_obj.get("songs") and artist_obj["songs"].get("browseId"): - prov_playlist_id = artist_obj["songs"]["browseId"] - playlist_tracks = await self.get_playlist_tracks(prov_playlist_id) - return playlist_tracks[:25] - return [] - - async def library_add(self, item: MediaItemType) -> bool: - """Add an item to the library.""" - await self._check_oauth_token() - result = False - if item.media_type == MediaType.ARTIST: - result = await library_add_remove_artist( - headers=self._headers, prov_artist_id=item.item_id, add=True - ) - elif item.media_type == MediaType.ALBUM: - result = await library_add_remove_album( - headers=self._headers, prov_item_id=item.item_id, add=True - ) - elif item.media_type == MediaType.PLAYLIST: - result = await library_add_remove_playlist( - headers=self._headers, prov_item_id=item.item_id, add=True - ) - elif item.media_type == MediaType.TRACK: - raise NotImplementedError - return result - - async def library_remove(self, prov_item_id, media_type: MediaType): - """Remove an item from the library.""" - await self._check_oauth_token() - result = False - try: - if media_type == MediaType.ARTIST: - result = await library_add_remove_artist( - headers=self._headers, prov_artist_id=prov_item_id, add=False - ) - elif media_type == MediaType.ALBUM: - result = await library_add_remove_album( - headers=self._headers, prov_item_id=prov_item_id, add=False - ) - elif media_type == MediaType.PLAYLIST: - result = await library_add_remove_playlist( - headers=self._headers, prov_item_id=prov_item_id, add=False - ) - elif media_type == MediaType.TRACK: - raise NotImplementedError - except YTMusicServerError as err: - # YTM raises if trying to remove an item that is not in the library - raise NotImplementedError(err) from err - return result - - async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add track(s) to playlist.""" - await self._check_oauth_token() - # Grab the playlist id from the full url in case of personal playlists - if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: - prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] - return await add_remove_playlist_tracks( - headers=self._headers, - prov_playlist_id=prov_playlist_id, - prov_track_ids=prov_track_ids, - add=True, - ) - - async def remove_playlist_tracks( - self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove track(s) from playlist.""" - await self._check_oauth_token() - # Grab the playlist id from the full url in case of personal playlists - if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: - prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] - playlist_obj = await get_playlist(prov_playlist_id=prov_playlist_id, headers=self._headers) - if "tracks" not in playlist_obj: - return None - tracks_to_delete = [] - for index, track in enumerate(playlist_obj["tracks"]): - if index in positions_to_remove: - # YT needs both the videoId and the setVideoId in order to remove - # the track. Thus, we need to obtain the playlist details and - # grab the info from there. - tracks_to_delete.append( - {"videoId": track["videoId"], "setVideoId": track["setVideoId"]} - ) - - return await add_remove_playlist_tracks( - headers=self._headers, - prov_playlist_id=prov_playlist_id, - prov_track_ids=tracks_to_delete, - add=False, - ) - - async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: - """Retrieve a dynamic list of tracks based on the provided item.""" - await self._check_oauth_token() - result = [] - result = await get_song_radio_tracks( - headers=self._headers, - prov_item_id=prov_track_id, - limit=limit, - ) - if "tracks" in result: - tracks = [] - for track in result["tracks"]: - # Playlist tracks sometimes do not have a valid artist id - # In that case, call the API for track details based on track id - try: - track = self._parse_track(track) - if track: - tracks.append(track) - except InvalidDataError: - if track := await self.get_track(track["videoId"]): - tracks.append(track) - return tracks - return [] - - async def get_stream_details(self, item_id: str) -> StreamDetails: - """Return the content details for the given track when it will be streamed.""" - stream_format = await self._get_stream_format(item_id=item_id) - self.logger.debug("Found stream_format: %s for song %s", stream_format["format"], item_id) - stream_details = StreamDetails( - provider=self.instance_id, - item_id=item_id, - audio_format=AudioFormat( - content_type=ContentType.try_parse(stream_format["audio_ext"]), - ), - stream_type=StreamType.HTTP, - path=stream_format["url"], - ) - if ( - stream_format.get("audio_channels") - and str(stream_format.get("audio_channels")).isdigit() - ): - stream_details.audio_format.channels = int(stream_format.get("audio_channels")) - if stream_format.get("asr"): - stream_details.audio_format.sample_rate = int(stream_format.get("asr")) - return stream_details - - async def _post_data(self, endpoint: str, data: dict[str, str], **kwargs): - """Post data to the given endpoint.""" - await self._check_oauth_token() - url = f"{YTM_BASE_URL}{endpoint}" - data.update(self._context) - async with self.mass.http_session.post( - url, - headers=self._headers, - json=data, - ssl=False, - cookies=self._cookies, - ) as response: - return await response.json() - - async def _get_data(self, url: str, params: dict | None = None): - """Get data from the given URL.""" - await self._check_oauth_token() - async with self.mass.http_session.get( - url, headers=self._headers, params=params, cookies=self._cookies - ) as response: - return await response.text() - - async def _check_oauth_token(self) -> None: - """Verify the OAuth token is valid and refresh if needed.""" - if self.config.get_value(CONF_EXPIRY_TIME) < time(): - token = await refresh_oauth_token( - self.mass.http_session, self.config.get_value(CONF_REFRESH_TOKEN) - ) - self.config.update({CONF_AUTH_TOKEN: token["access_token"]}) - self.config.update({CONF_EXPIRY_TIME: time() + token["expires_in"]}) - self.config.update({CONF_TOKEN_TYPE: token["token_type"]}) - self._initialize_headers() - - def _initialize_headers(self) -> dict[str, str]: - """Return headers to include in the requests.""" - auth = f"{self.config.get_value(CONF_TOKEN_TYPE)} {self.config.get_value(CONF_AUTH_TOKEN)}" - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", # noqa: E501 - "Accept": "*/*", - "Accept-Language": "en-US,en;q=0.5", - "Content-Type": "application/json", - "X-Goog-AuthUser": "0", - "x-origin": YTM_DOMAIN, - "X-Goog-Request-Time": str(int(time())), - "Authorization": auth, - } - self._headers = headers - - def _initialize_context(self) -> dict[str, str]: - """Return a dict to use as a context in requests.""" - self._context = { - "context": { - "client": {"clientName": "WEB_REMIX", "clientVersion": "0.1"}, - "user": {}, - } - } - - def _parse_album(self, album_obj: dict, album_id: str | None = None) -> Album: - """Parse a YT Album response to an Album model object.""" - album_id = album_id or album_obj.get("id") or album_obj.get("browseId") - if "title" in album_obj: - name = album_obj["title"] - elif "name" in album_obj: - name = album_obj["name"] - album = Album( - item_id=album_id, - name=name, - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=str(album_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f"{YTM_DOMAIN}/playlist?list={album_id}", - ) - }, - ) - if album_obj.get("year") and album_obj["year"].isdigit(): - album.year = album_obj["year"] - if "thumbnails" in album_obj: - album.metadata.images = self._parse_thumbnails(album_obj["thumbnails"]) - if description := album_obj.get("description"): - album.metadata.description = unquote(description) - if "isExplicit" in album_obj: - album.metadata.explicit = album_obj["isExplicit"] - if "artists" in album_obj: - album.artists = [ - self._get_artist_item_mapping(artist) - for artist in album_obj["artists"] - if artist.get("id") - or artist.get("channelId") - or artist.get("name") == "Various Artists" - ] - if "type" in album_obj: - if album_obj["type"] == "Single": - album_type = AlbumType.SINGLE - elif album_obj["type"] == "EP": - album_type = AlbumType.EP - elif album_obj["type"] == "Album": - album_type = AlbumType.ALBUM - else: - album_type = AlbumType.UNKNOWN - album.album_type = album_type - return album - - def _parse_artist(self, artist_obj: dict) -> Artist: - """Parse a YT Artist response to Artist model object.""" - artist_id = None - if "channelId" in artist_obj: - artist_id = artist_obj["channelId"] - elif artist_obj.get("id"): - artist_id = artist_obj["id"] - elif artist_obj["name"] == "Various Artists": - artist_id = VARIOUS_ARTISTS_YTM_ID - if not artist_id: - msg = "Artist does not have a valid ID" - raise InvalidDataError(msg) - artist = Artist( - item_id=artist_id, - name=artist_obj["name"], - provider=self.domain, - provider_mappings={ - ProviderMapping( - item_id=str(artist_id), - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f"{YTM_DOMAIN}/channel/{artist_id}", - ) - }, - ) - if "description" in artist_obj: - artist.metadata.description = artist_obj["description"] - if artist_obj.get("thumbnails"): - artist.metadata.images = self._parse_thumbnails(artist_obj["thumbnails"]) - return artist - - def _parse_playlist(self, playlist_obj: dict) -> Playlist: - """Parse a YT Playlist response to a Playlist object.""" - playlist_id = playlist_obj["id"] - playlist_name = playlist_obj["title"] - # Playlist ID's are not unique across instances for lists like 'Likes', 'Supermix', etc. - # So suffix with the instance id to make them unique - if playlist_id in YT_PERSONAL_PLAYLISTS: - playlist_id = f"{playlist_id}{YT_PLAYLIST_ID_DELIMITER}{self.instance_id}" - playlist_name = f"{playlist_name} ({self.name})" - playlist = Playlist( - item_id=playlist_id, - provider=self.domain, - name=playlist_name, - provider_mappings={ - ProviderMapping( - item_id=playlist_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=f"{YTM_DOMAIN}/playlist?list={playlist_id}", - ) - }, - ) - if "description" in playlist_obj: - playlist.metadata.description = playlist_obj["description"] - if playlist_obj.get("thumbnails"): - playlist.metadata.images = self._parse_thumbnails(playlist_obj["thumbnails"]) - is_editable = False - if playlist_obj.get("privacy") and playlist_obj.get("privacy") == "PRIVATE": - is_editable = True - playlist.is_editable = is_editable - if authors := playlist_obj.get("author"): - if isinstance(authors, str): - playlist.owner = authors - elif isinstance(authors, list): - playlist.owner = authors[0]["name"] - else: - playlist.owner = authors["name"] - else: - playlist.owner = self.name - playlist.cache_checksum = playlist_obj.get("checksum") - return playlist - - def _parse_track(self, track_obj: dict) -> Track: - """Parse a YT Track response to a Track model object.""" - if not track_obj.get("videoId"): - msg = "Track is missing videoId" - raise InvalidDataError(msg) - track_id = str(track_obj["videoId"]) - track = Track( - item_id=track_id, - provider=self.domain, - name=track_obj["title"], - provider_mappings={ - ProviderMapping( - item_id=track_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - available=track_obj.get("isAvailable", True), - url=f"{YTM_DOMAIN}/watch?v={track_id}", - audio_format=AudioFormat( - content_type=ContentType.M4A, - ), - ) - }, - disc_number=0, # not supported on YTM? - track_number=track_obj.get("trackNumber", 0), - ) - - if track_obj.get("artists"): - track.artists = [ - self._get_artist_item_mapping(artist) - for artist in track_obj["artists"] - if artist.get("id") - or artist.get("channelId") - or artist.get("name") == "Various Artists" - ] - # guard that track has valid artists - if not track.artists: - msg = "Track is missing artists" - raise InvalidDataError(msg) - if track_obj.get("thumbnails"): - track.metadata.images = self._parse_thumbnails(track_obj["thumbnails"]) - if ( - track_obj.get("album") - and isinstance(track_obj.get("album"), dict) - and track_obj["album"].get("id") - ): - album = track_obj["album"] - track.album = self._get_item_mapping(MediaType.ALBUM, album["id"], album["name"]) - if "isExplicit" in track_obj: - track.metadata.explicit = track_obj["isExplicit"] - if "duration" in track_obj and str(track_obj["duration"]).isdigit(): - track.duration = int(track_obj["duration"]) - elif "duration_seconds" in track_obj and str(track_obj["duration_seconds"]).isdigit(): - track.duration = int(track_obj["duration_seconds"]) - return track - - async def _get_stream_format(self, item_id: str) -> dict[str, Any]: - """Figure out the stream URL to use and return the highest quality.""" - await self._check_oauth_token() - - def _extract_best_stream_url_format() -> dict[str, Any]: - url = f"{YTM_DOMAIN}/watch?v={item_id}" - auth = ( - f"{self.config.get_value(CONF_TOKEN_TYPE)} {self.config.get_value(CONF_AUTH_TOKEN)}" - ) - ydl_opts = { - "quiet": self.logger.level > logging.DEBUG, - # This enables the access token plugin so we can grab the best - # available quality audio stream - "username": auth, - # This enforces a player client and skips unnecessary scraping to increase speed - "extractor_args": { - "youtube": {"skip": ["translated_subs", "dash"], "player_client": ["ios"]} - }, - } - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - try: - info = ydl.extract_info(url, download=False) - except yt_dlp.utils.DownloadError as err: - raise UnplayableMediaError(err) from err - format_selector = ydl.build_format_selector("m4a/bestaudio") - stream_format = next(format_selector({"formats": info["formats"]})) - return stream_format - - return await asyncio.to_thread(_extract_best_stream_url_format) - - def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: - return ItemMapping( - media_type=media_type, - item_id=key, - provider=self.instance_id, - name=name, - ) - - def _get_artist_item_mapping(self, artist_obj: dict) -> ItemMapping: - artist_id = artist_obj.get("id") or artist_obj.get("channelId") - if not artist_id and artist_obj["name"] == "Various Artists": - artist_id = VARIOUS_ARTISTS_YTM_ID - return self._get_item_mapping(MediaType.ARTIST, artist_id, artist_obj.get("name")) - - async def _user_has_ytm_premium(self) -> bool: - """Check if the user has Youtube Music Premium.""" - stream_format = await self._get_stream_format(YTM_PREMIUM_CHECK_TRACK_ID) - # Only premium users can stream the HQ stream of this song - return stream_format["format_id"] == "141" - - def _parse_thumbnails(self, thumbnails_obj: dict) -> list[MediaItemImage]: - """Parse and YTM thumbnails to MediaItemImage.""" - result: list[MediaItemImage] = [] - processed_images = set() - for img in sorted(thumbnails_obj, key=lambda w: w.get("width", 0), reverse=True): - url: str = img["url"] - url_base = url.split("=w")[0] - width: int = img["width"] - height: int = img["height"] - image_ratio: float = width / height - image_type = ( - ImageType.LANDSCAPE - if "maxresdefault" in url or image_ratio > 2.0 - else ImageType.THUMB - ) - if "=w" not in url and width < 500: - continue - # if the size is in the url, we can actually request a higher thumb - if "=w" in url and width < 600: - url = f"{url_base}=w600-h600-p" - image_type = ImageType.THUMB - if (url_base, image_type) in processed_images: - continue - processed_images.add((url_base, image_type)) - result.append( - MediaItemImage( - type=image_type, - path=url, - provider=self.lookup_key, - remotely_accessible=True, - ) - ) - return result diff --git a/music_assistant/server/providers/ytmusic/helpers.py b/music_assistant/server/providers/ytmusic/helpers.py deleted file mode 100644 index 6af6dc7d..00000000 --- a/music_assistant/server/providers/ytmusic/helpers.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Helper module for parsing the Youtube Music API. - -This helpers file is an async wrapper around the excellent ytmusicapi package. -While the ytmusicapi package does an excellent job at parsing the Youtube Music results, -it is unfortunately not async, which is required for Music Assistant to run smoothly. -This also nicely separates the parsing logic from the Youtube Music provider logic. -""" - -import asyncio -from time import time - -import ytmusicapi -from aiohttp import ClientSession -from ytmusicapi.constants import ( - OAUTH_CLIENT_ID, - OAUTH_CLIENT_SECRET, - OAUTH_CODE_URL, - OAUTH_SCOPE, - OAUTH_TOKEN_URL, - OAUTH_USER_AGENT, -) - -from music_assistant.server.helpers.auth import AuthenticationHelper - - -async def get_artist( - prov_artist_id: str, headers: dict[str, str], language: str = "en" -) -> dict[str, str]: - """Async wrapper around the ytmusicapi get_artist function.""" - - def _get_artist(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - try: - artist = ytm.get_artist(channelId=prov_artist_id) - # ChannelId can sometimes be different and original ID is not part of the response - artist["channelId"] = prov_artist_id - except KeyError: - try: - user = ytm.get_user(channelId=prov_artist_id) - artist = {"channelId": prov_artist_id, "name": user["name"]} - except KeyError: - artist = {"channelId": prov_artist_id, "name": "Unknown"} - return artist - - return await asyncio.to_thread(_get_artist) - - -async def get_album(prov_album_id: str, language: str = "en") -> dict[str, str]: - """Async wrapper around the ytmusicapi get_album function.""" - - def _get_album(): - ytm = ytmusicapi.YTMusic(language=language) - return ytm.get_album(browseId=prov_album_id) - - return await asyncio.to_thread(_get_album) - - -async def get_playlist( - prov_playlist_id: str, headers: dict[str, str], language: str = "en" -) -> dict[str, str]: - """Async wrapper around the ytmusicapi get_playlist function.""" - - def _get_playlist(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - playlist = ytm.get_playlist(playlistId=prov_playlist_id, limit=None) - playlist["checksum"] = get_playlist_checksum(playlist) - # Fix missing playlist id in some edge cases - playlist["id"] = prov_playlist_id if not playlist.get("id") else playlist["id"] - return playlist - - return await asyncio.to_thread(_get_playlist) - - -async def get_track( - prov_track_id: str, headers: dict[str, str], language: str = "en" -) -> dict[str, str] | None: - """Async wrapper around the ytmusicapi get_playlist function.""" - - def _get_song(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - track_obj = ytm.get_song(videoId=prov_track_id) - track = {} - if "videoDetails" not in track_obj: - # video that no longer exists - return None - track["videoId"] = track_obj["videoDetails"]["videoId"] - track["title"] = track_obj["videoDetails"]["title"] - track["artists"] = [ - { - "channelId": track_obj["videoDetails"]["channelId"], - "name": track_obj["videoDetails"]["author"], - } - ] - track["duration"] = track_obj["videoDetails"]["lengthSeconds"] - track["thumbnails"] = track_obj["microformat"]["microformatDataRenderer"]["thumbnail"][ - "thumbnails" - ] - track["isAvailable"] = track_obj["playabilityStatus"]["status"] == "OK" - return track - - return await asyncio.to_thread(_get_song) - - -async def get_library_artists(headers: dict[str, str], language: str = "en") -> dict[str, str]: - """Async wrapper around the ytmusicapi get_library_artists function.""" - - def _get_library_artists(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - artists = ytm.get_library_subscriptions(limit=9999) - # Sync properties with uniformal artist object - for artist in artists: - artist["id"] = artist["browseId"] - artist["name"] = artist["artist"] - del artist["browseId"] - del artist["artist"] - return artists - - return await asyncio.to_thread(_get_library_artists) - - -async def get_library_albums(headers: dict[str, str], language: str = "en") -> dict[str, str]: - """Async wrapper around the ytmusicapi get_library_albums function.""" - - def _get_library_albums(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - return ytm.get_library_albums(limit=9999) - - return await asyncio.to_thread(_get_library_albums) - - -async def get_library_playlists(headers: dict[str, str], language: str = "en") -> dict[str, str]: - """Async wrapper around the ytmusicapi get_library_playlists function.""" - - def _get_library_playlists(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - playlists = ytm.get_library_playlists(limit=9999) - # Sync properties with uniformal playlist object - for playlist in playlists: - playlist["id"] = playlist["playlistId"] - del playlist["playlistId"] - playlist["checksum"] = get_playlist_checksum(playlist) - return playlists - - return await asyncio.to_thread(_get_library_playlists) - - -async def get_library_tracks(headers: dict[str, str], language: str = "en") -> dict[str, str]: - """Async wrapper around the ytmusicapi get_library_tracks function.""" - - def _get_library_tracks(): - ytm = ytmusicapi.YTMusic(auth=headers, language=language) - return ytm.get_library_songs(limit=9999) - - return await asyncio.to_thread(_get_library_tracks) - - -async def library_add_remove_artist( - headers: dict[str, str], prov_artist_id: str, add: bool = True -) -> bool: - """Add or remove an artist to the user's library.""" - - def _library_add_remove_artist(): - ytm = ytmusicapi.YTMusic(auth=headers) - if add: - return "actions" in ytm.subscribe_artists(channelIds=[prov_artist_id]) - if not add: - return "actions" in ytm.unsubscribe_artists(channelIds=[prov_artist_id]) - return None - - return await asyncio.to_thread(_library_add_remove_artist) - - -async def library_add_remove_album( - headers: dict[str, str], prov_item_id: str, add: bool = True -) -> bool: - """Add or remove an album or playlist to the user's library.""" - album = await get_album(prov_album_id=prov_item_id) - - def _library_add_remove_album(): - ytm = ytmusicapi.YTMusic(auth=headers) - playlist_id = album["audioPlaylistId"] - if add: - return ytm.rate_playlist(playlist_id, "LIKE") - if not add: - return ytm.rate_playlist(playlist_id, "INDIFFERENT") - return None - - return await asyncio.to_thread(_library_add_remove_album) - - -async def library_add_remove_playlist( - headers: dict[str, str], prov_item_id: str, add: bool = True -) -> bool: - """Add or remove an album or playlist to the user's library.""" - - def _library_add_remove_playlist(): - ytm = ytmusicapi.YTMusic(auth=headers) - if add: - return "actions" in ytm.rate_playlist(prov_item_id, "LIKE") - if not add: - return "actions" in ytm.rate_playlist(prov_item_id, "INDIFFERENT") - return None - - return await asyncio.to_thread(_library_add_remove_playlist) - - -async def add_remove_playlist_tracks( - headers: dict[str, str], prov_playlist_id: str, prov_track_ids: list[str], add: bool -) -> bool: - """Async wrapper around adding/removing tracks to a playlist.""" - - def _add_playlist_tracks(): - ytm = ytmusicapi.YTMusic(auth=headers) - if add: - return ytm.add_playlist_items(playlistId=prov_playlist_id, videoIds=prov_track_ids) - if not add: - return ytm.remove_playlist_items(playlistId=prov_playlist_id, videos=prov_track_ids) - return None - - return await asyncio.to_thread(_add_playlist_tracks) - - -async def get_song_radio_tracks( - headers: dict[str, str], prov_item_id: str, limit=25 -) -> dict[str, str]: - """Async wrapper around the ytmusicapi radio function.""" - - def _get_song_radio_tracks(): - ytm = ytmusicapi.YTMusic(auth=headers) - playlist_id = f"RDAMVM{prov_item_id}" - result = ytm.get_watch_playlist( - videoId=prov_item_id, playlistId=playlist_id, limit=limit, radio=True - ) - # Replace inconsistensies for easier parsing - for track in result["tracks"]: - if track.get("thumbnail"): - track["thumbnails"] = track["thumbnail"] - del track["thumbnail"] - if track.get("length"): - track["duration"] = get_sec(track["length"]) - return result - - return await asyncio.to_thread(_get_song_radio_tracks) - - -async def search( - query: str, ytm_filter: str | None = None, limit: int = 20, language: str = "en" -) -> list[dict]: - """Async wrapper around the ytmusicapi search function.""" - - def _search(): - ytm = ytmusicapi.YTMusic(language=language) - results = ytm.search(query=query, filter=ytm_filter, limit=limit) - # Sync result properties with uniformal objects - for result in results: - if result["resultType"] == "artist": - if "artists" in result and len(result["artists"]) > 0: - result["id"] = result["artists"][0]["id"] - result["name"] = result["artists"][0]["name"] - del result["artists"] - else: - result["id"] = result["browseId"] - result["name"] = result["artist"] - del result["browseId"] - del result["artist"] - elif result["resultType"] == "playlist": - if "playlistId" in result: - result["id"] = result["playlistId"] - del result["playlistId"] - elif "browseId" in result: - result["id"] = result["browseId"] - del result["browseId"] - return results[:limit] - - return await asyncio.to_thread(_search) - - -def get_playlist_checksum(playlist_obj: dict) -> str: - """Try to calculate a checksum so we can detect changes in a playlist.""" - for key in ("duration_seconds", "trackCount", "count"): - if key in playlist_obj: - return playlist_obj[key] - return str(int(time())) - - -def is_brand_account(username: str) -> bool: - """Check if the provided username is a brand-account.""" - return len(username) == 21 and username.isdigit() - - -def get_sec(time_str): - """Get seconds from time.""" - parts = time_str.split(":") - if len(parts) == 3: - return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) - if len(parts) == 2: - return int(parts[0]) * 60 + int(parts[1]) - return 0 - - -async def login_oauth(auth_helper: AuthenticationHelper): - """Use device login to get a token.""" - http_session = auth_helper.mass.http_session - code = await get_oauth_code(http_session) - return await visit_oauth_auth_url(auth_helper, code) - - -def _get_data_and_headers(data: dict): - """Prepare headers for OAuth requests.""" - data.update({"client_id": OAUTH_CLIENT_ID}) - headers = {"User-Agent": OAUTH_USER_AGENT} - return data, headers - - -async def get_oauth_code(session: ClientSession): - """Get the OAuth code from the server.""" - data, headers = _get_data_and_headers({"scope": OAUTH_SCOPE}) - async with session.post(OAUTH_CODE_URL, json=data, headers=headers) as code_response: - return await code_response.json() - - -async def visit_oauth_auth_url(auth_helper: AuthenticationHelper, code: dict[str, str]): - """Redirect the user to the OAuth login page and wait for the token.""" - auth_url = f"{code['verification_url']}?user_code={code['user_code']}" - auth_helper.send_url(auth_url=auth_url) - device_code = code["device_code"] - expiry = code["expires_in"] - interval = code["interval"] - while expiry > 0: - token = await get_oauth_token_from_code(auth_helper.mass.http_session, device_code) - if token.get("access_token"): - return token - await asyncio.sleep(interval) - expiry -= interval - msg = "You took too long to log in" - raise TimeoutError(msg) - - -async def get_oauth_token_from_code(session: ClientSession, device_code: str): - """Check if the OAuth token is ready yet.""" - data, headers = _get_data_and_headers( - data={ - "client_secret": OAUTH_CLIENT_SECRET, - "grant_type": "http://oauth.net/grant_type/device/1.0", - "code": device_code, - } - ) - async with session.post( - OAUTH_TOKEN_URL, - json=data, - headers=headers, - ) as token_response: - return await token_response.json() - - -async def refresh_oauth_token(session: ClientSession, refresh_token: str): - """Refresh an expired OAuth token.""" - data, headers = _get_data_and_headers( - { - "client_secret": OAUTH_CLIENT_SECRET, - "grant_type": "refresh_token", - "refresh_token": refresh_token, - } - ) - async with session.post( - OAUTH_TOKEN_URL, - json=data, - headers=headers, - ) as response: - return await response.json() diff --git a/music_assistant/server/providers/ytmusic/icon.svg b/music_assistant/server/providers/ytmusic/icon.svg deleted file mode 100644 index 22ba913e..00000000 --- a/music_assistant/server/providers/ytmusic/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/music_assistant/server/providers/ytmusic/manifest.json b/music_assistant/server/providers/ytmusic/manifest.json deleted file mode 100644 index 30d02f38..00000000 --- a/music_assistant/server/providers/ytmusic/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "music", - "domain": "ytmusic", - "name": "YouTube Music", - "description": "Support for the YouTube Music streaming provider in Music Assistant.", - "codeowners": ["@MarvinSchenkel"], - "requirements": ["ytmusicapi==1.8.1", "yt-dlp-youtube-accesstoken==0.1.1", "yt-dlp==2024.10.7"], - "documentation": "https://music-assistant.io/music-providers/youtube-music/", - "multi_instance": true -} diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py deleted file mode 100644 index 48e9e880..00000000 --- a/music_assistant/server/server.py +++ /dev/null @@ -1,772 +0,0 @@ -"""Main Music Assistant class.""" - -from __future__ import annotations - -import asyncio -import logging -import os -from collections.abc import Awaitable, Callable, Coroutine -from typing import TYPE_CHECKING, Any, Self -from uuid import uuid4 - -import aiofiles -from aiofiles.os import wrap -from aiohttp import ClientSession, TCPConnector -from zeroconf import IPVersion, NonUniqueNameException, ServiceStateChange, Zeroconf -from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf - -from music_assistant.common.helpers.global_cache import set_global_cache_values -from music_assistant.common.helpers.util import get_ip_pton -from music_assistant.common.models.api import ServerInfoMessage -from music_assistant.common.models.enums import EventType, ProviderType -from music_assistant.common.models.errors import MusicAssistantError, SetupFailedError -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.constants import ( - API_SCHEMA_VERSION, - CONF_PROVIDERS, - CONF_SERVER_ID, - CONFIGURABLE_CORE_CONTROLLERS, - MASS_LOGGER_NAME, - MIN_SCHEMA_VERSION, - VERBOSE_LOG_LEVEL, -) -from music_assistant.server.controllers.cache import CacheController -from music_assistant.server.controllers.config import ConfigController -from music_assistant.server.controllers.metadata import MetaDataController -from music_assistant.server.controllers.music import MusicController -from music_assistant.server.controllers.player_queues import PlayerQueuesController -from music_assistant.server.controllers.players import PlayerController -from music_assistant.server.controllers.streams import StreamsController -from music_assistant.server.controllers.webserver import WebserverController -from music_assistant.server.helpers.api import APICommandHandler, api_command -from music_assistant.server.helpers.images import get_icon_string -from music_assistant.server.helpers.util import ( - TaskManager, - get_package_version, - is_hass_supervisor, - load_provider_module, -) - -from .models import ProviderInstanceType - -if TYPE_CHECKING: - from types import TracebackType - - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.server.models.core_controller import CoreController - -isdir = wrap(os.path.isdir) -isfile = wrap(os.path.isfile) - -EventCallBackType = Callable[[MassEvent], None] -EventSubscriptionType = tuple[ - EventCallBackType, tuple[EventType, ...] | None, tuple[str, ...] | None -] - -ENABLE_DEBUG = os.environ.get("PYTHONDEVMODE") == "1" -LOGGER = logging.getLogger(MASS_LOGGER_NAME) - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -PROVIDERS_PATH = os.path.join(BASE_DIR, "providers") - - -class MusicAssistant: - """Main MusicAssistant (Server) object.""" - - loop: asyncio.AbstractEventLoop - http_session: ClientSession - aiozc: AsyncZeroconf - config: ConfigController - webserver: WebserverController - cache: CacheController - metadata: MetaDataController - music: MusicController - players: PlayerController - player_queues: PlayerQueuesController - streams: StreamsController - _aiobrowser: AsyncServiceBrowser - - def __init__(self, storage_path: str, safe_mode: bool = False) -> None: - """Initialize the MusicAssistant Server.""" - self.storage_path = storage_path - self.safe_mode = safe_mode - # we dynamically register command handlers which can be consumed by the apis - self.command_handlers: dict[str, APICommandHandler] = {} - self._subscribers: set[EventSubscriptionType] = set() - self._provider_manifests: dict[str, ProviderManifest] = {} - self._providers: dict[str, ProviderInstanceType] = {} - self._tracked_tasks: dict[str, asyncio.Task] = {} - self._tracked_timers: dict[str, asyncio.TimerHandle] = {} - self.closing = False - self.running_as_hass_addon: bool = False - self.version: str = "0.0.0" - - async def start(self) -> None: - """Start running the Music Assistant server.""" - self.loop = asyncio.get_running_loop() - self.running_as_hass_addon = await is_hass_supervisor() - self.version = await get_package_version("music_assistant") or "0.0.0" - # create shared zeroconf instance - # TODO: enumerate interfaces and enable IPv6 support - self.aiozc = AsyncZeroconf(ip_version=IPVersion.V4Only) - # create shared aiohttp ClientSession - self.http_session = ClientSession( - loop=self.loop, - connector=TCPConnector( - ssl=False, - enable_cleanup_closed=True, - limit=4096, - limit_per_host=100, - ), - ) - # setup config controller first and fetch important config values - self.config = ConfigController(self) - await self.config.setup() - LOGGER.info( - "Starting Music Assistant Server (%s) version %s - HA add-on: %s - Safe mode: %s", - self.server_id, - self.version, - self.running_as_hass_addon, - self.safe_mode, - ) - # setup other core controllers - self.cache = CacheController(self) - self.webserver = WebserverController(self) - self.metadata = MetaDataController(self) - self.music = MusicController(self) - self.players = PlayerController(self) - self.player_queues = PlayerQueuesController(self) - self.streams = StreamsController(self) - # add manifests for core controllers - for controller_name in CONFIGURABLE_CORE_CONTROLLERS: - controller: CoreController = getattr(self, controller_name) - self._provider_manifests[controller.domain] = controller.manifest - await self.cache.setup(await self.config.get_core_config("cache")) - await self.music.setup(await self.config.get_core_config("music")) - await self.metadata.setup(await self.config.get_core_config("metadata")) - await self.players.setup(await self.config.get_core_config("players")) - await self.player_queues.setup(await self.config.get_core_config("player_queues")) - # load streams and webserver last so the api/frontend is - # not yet available while we're starting (or performing migrations) - self._register_api_commands() - await self.streams.setup(await self.config.get_core_config("streams")) - await self.webserver.setup(await self.config.get_core_config("webserver")) - # load all available providers from manifest files - await self.__load_provider_manifests() - # setup discovery - await self._setup_discovery() - # load providers - if not self.safe_mode: - await self._load_providers() - - async def stop(self) -> None: - """Stop running the music assistant server.""" - LOGGER.info("Stop called, cleaning up...") - self.signal_event(EventType.SHUTDOWN) - self.closing = True - # cancel all running tasks - for task in self._tracked_tasks.values(): - task.cancel() - # cleanup all providers - await asyncio.gather( - *[self.unload_provider(prov_id) for prov_id in list(self._providers.keys())], - return_exceptions=True, - ) - # stop core controllers - await self.streams.close() - await self.webserver.close() - await self.metadata.close() - await self.music.close() - await self.player_queues.close() - await self.players.close() - # cleanup cache and config - await self.config.close() - await self.cache.close() - # close/cleanup shared http session - if self.http_session: - await self.http_session.close() - - @property - def server_id(self) -> str: - """Return unique ID of this server.""" - if not self.config.initialized: - return "" - return self.config.get(CONF_SERVER_ID) # type: ignore[no-any-return] - - @api_command("info") - def get_server_info(self) -> ServerInfoMessage: - """Return Info of this server.""" - return ServerInfoMessage( - server_id=self.server_id, - server_version=self.version, - schema_version=API_SCHEMA_VERSION, - min_supported_schema_version=MIN_SCHEMA_VERSION, - base_url=self.webserver.base_url, - homeassistant_addon=self.running_as_hass_addon, - onboard_done=self.config.onboard_done, - ) - - @api_command("providers/manifests") - def get_provider_manifests(self) -> list[ProviderManifest]: - """Return all Provider manifests.""" - return list(self._provider_manifests.values()) - - @api_command("providers/manifests/get") - def get_provider_manifest(self, domain: str) -> ProviderManifest: - """Return Provider manifests of single provider(domain).""" - return self._provider_manifests[domain] - - @api_command("providers") - def get_providers( - self, provider_type: ProviderType | None = None - ) -> list[ProviderInstanceType]: - """Return all loaded/running Providers (instances), optionally filtered by ProviderType.""" - return [ - x for x in self._providers.values() if provider_type is None or provider_type == x.type - ] - - @api_command("logging/get") - async def get_application_log(self) -> str: - """Return the application log from file.""" - logfile = os.path.join(self.storage_path, "musicassistant.log") - async with aiofiles.open(logfile) as _file: - return await _file.read() - - @property - def providers(self) -> list[ProviderInstanceType]: - """Return all loaded/running Providers (instances).""" - return list(self._providers.values()) - - def get_provider( - self, provider_instance_or_domain: str, return_unavailable: bool = False - ) -> ProviderInstanceType | None: - """Return provider by instance id or domain.""" - # lookup by instance_id first - if prov := self._providers.get(provider_instance_or_domain): - if return_unavailable or prov.available: - return prov - if not getattr(prov, "is_streaming_provider", None): - # no need to lookup other instances because this provider has unique data - return None - provider_instance_or_domain = prov.domain - # fallback to match on domain - for prov in self._providers.values(): - if prov.domain != provider_instance_or_domain: - continue - if return_unavailable or prov.available: - return prov - return None - - def signal_event( - self, - event: EventType, - object_id: str | None = None, - data: Any = None, - ) -> None: - """Signal event to subscribers.""" - if self.closing: - return - - if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL): - # do not log queue time updated events because that is too chatty - LOGGER.getChild("event").log(VERBOSE_LOG_LEVEL, "%s %s", event.value, object_id or "") - - event_obj = MassEvent(event=event, object_id=object_id, data=data) - for cb_func, event_filter, id_filter in self._subscribers: - if not (event_filter is None or event in event_filter): - continue - if not (id_filter is None or object_id in id_filter): - continue - if asyncio.iscoroutinefunction(cb_func): - asyncio.run_coroutine_threadsafe(cb_func(event_obj), self.loop) - else: - self.loop.call_soon_threadsafe(cb_func, event_obj) - - def subscribe( - self, - cb_func: EventCallBackType, - event_filter: EventType | tuple[EventType, ...] | None = None, - id_filter: str | tuple[str, ...] | None = None, - ) -> Callable: - """Add callback to event listeners. - - Returns function to remove the listener. - :param cb_func: callback function or coroutine - :param event_filter: Optionally only listen for these events - :param id_filter: Optionally only listen for these id's (player_id, queue_id, uri) - """ - if isinstance(event_filter, EventType): - event_filter = (event_filter,) - if isinstance(id_filter, str): - id_filter = (id_filter,) - listener = (cb_func, event_filter, id_filter) - self._subscribers.add(listener) - - def remove_listener() -> None: - self._subscribers.remove(listener) - - return remove_listener - - def create_task( - self, - target: Coroutine | Awaitable | Callable, - *args: Any, - task_id: str | None = None, - abort_existing: bool = False, - **kwargs: Any, - ) -> asyncio.Task | asyncio.Future: - """Create Task on (main) event loop from Coroutine(function). - - Tasks created by this helper will be properly cancelled on stop. - """ - if target is None: - msg = "Target is missing" - raise RuntimeError(msg) - if task_id and (existing := self._tracked_tasks.get(task_id)) and not existing.done(): - # prevent duplicate tasks if task_id is given and already present - if abort_existing: - existing.cancel() - else: - return existing - if asyncio.iscoroutinefunction(target): - # coroutine function - task = self.loop.create_task(target(*args, **kwargs)) - elif asyncio.iscoroutine(target): - # coroutine - task = self.loop.create_task(target) - else: - task = self.loop.create_task(asyncio.to_thread(target, *args, **kwargs)) - - def task_done_callback(_task: asyncio.Task) -> None: - _task_id = task.task_id - self._tracked_tasks.pop(_task_id, None) - # log unhandled exceptions - if ( - LOGGER.isEnabledFor(logging.DEBUG) - and not _task.cancelled() - and (err := _task.exception()) - ): - task_name = _task.get_name() if hasattr(_task, "get_name") else str(_task) - LOGGER.warning( - "Exception in task %s - target: %s: %s", - task_name, - str(target), - str(err), - exc_info=err if LOGGER.isEnabledFor(logging.DEBUG) else None, - ) - - if task_id is None: - task_id = uuid4().hex - task.task_id = task_id - self._tracked_tasks[task_id] = task - task.add_done_callback(task_done_callback) - return task - - def call_later( - self, - delay: float, - target: Coroutine | Awaitable | Callable, - *args: Any, - task_id: str | None = None, - **kwargs: Any, - ) -> asyncio.TimerHandle: - """ - Run callable/awaitable after given delay. - - Use task_id for debouncing. - """ - if not task_id: - task_id = uuid4().hex - - if existing := self._tracked_timers.get(task_id): - existing.cancel() - - def _create_task() -> None: - self._tracked_timers.pop(task_id) - self.create_task(target, *args, task_id=task_id, abort_existing=True, **kwargs) - - handle = self.loop.call_later(delay, _create_task) - self._tracked_timers[task_id] = handle - return handle - - def get_task(self, task_id: str) -> asyncio.Task: - """Get existing scheduled task.""" - if existing := self._tracked_tasks.get(task_id): - # prevent duplicate tasks if task_id is given and already present - return existing - msg = "Task does not exist" - raise KeyError(msg) - - def register_api_command( - self, - command: str, - handler: Callable, - ) -> None: - """ - Dynamically register a command on the API. - - Returns handle to unregister. - """ - if command in self.command_handlers: - msg = f"Command {command} is already registered" - raise RuntimeError(msg) - self.command_handlers[command] = APICommandHandler.parse(command, handler) - - def unregister() -> None: - self.command_handlers.pop(command) - - return unregister - - async def load_provider_config( - self, - prov_conf: ProviderConfig, - ) -> None: - """Try to load a provider and catch errors.""" - # cancel existing (re)load timer if needed - task_id = f"load_provider_{prov_conf.instance_id}" - if existing := self._tracked_timers.pop(task_id, None): - existing.cancel() - - await self._load_provider(prov_conf) - - # (re)load any dependants - prov_configs = await self.config.get_provider_configs(include_values=True) - for dep_prov_conf in prov_configs: - if not dep_prov_conf.enabled: - continue - manifest = self.get_provider_manifest(dep_prov_conf.domain) - if not manifest.depends_on: - continue - if manifest.depends_on == prov_conf.domain: - await self._load_provider(dep_prov_conf) - - async def load_provider( - self, - instance_id: str, - allow_retry: bool = False, - ) -> None: - """Try to load a provider and catch errors.""" - try: - prov_conf = await self.config.get_provider_config(instance_id) - except KeyError: - # Was deleted before we could run - return - - if not prov_conf.enabled: - # Was disabled before we could run - return - - # cancel existing (re)load timer if needed - task_id = f"load_provider_{instance_id}" - if existing := self._tracked_timers.pop(task_id, None): - existing.cancel() - - try: - await self.load_provider_config(prov_conf) - except Exception as exc: - # if loading failed, we store the error in the config object - # so we can show something useful to the user - prov_conf.last_error = str(exc) - self.config.set(f"{CONF_PROVIDERS}/{instance_id}/last_error", str(exc)) - - # auto schedule a retry if the (re)load failed (handled exceptions only) - if isinstance(exc, MusicAssistantError) and allow_retry: - self.call_later( - 120, - self.load_provider, - instance_id, - allow_retry, - task_id=task_id, - ) - LOGGER.warning( - "Error loading provider(instance) %s: %s (will be retried later)", - prov_conf.name or prov_conf.instance_id, - str(exc) or exc.__class__.__name__, - # log full stack trace if verbose logging is enabled - exc_info=exc if LOGGER.isEnabledFor(VERBOSE_LOG_LEVEL) else None, - ) - return - # raise in all other situations - raise - - # (re)load any dependents if needed - for dep_prov in self.providers: - if dep_prov.available: - continue - if dep_prov.manifest.depends_on == prov_conf.domain: - await self.unload_provider(dep_prov.instance_id) - - async def unload_provider(self, instance_id: str) -> None: - """Unload a provider.""" - if provider := self._providers.get(instance_id): - # remove mdns discovery if needed - if provider.manifest.mdns_discovery: - for mdns_type in provider.manifest.mdns_discovery: - self._aiobrowser.types.discard(mdns_type) - # make sure to stop any running sync tasks first - for sync_task in self.music.in_progress_syncs: - if sync_task.provider_instance == instance_id: - sync_task.task.cancel() - # check if there are no other providers dependent of this provider - for dep_prov in self.providers: - if dep_prov.manifest.depends_on == provider.domain: - await self.unload_provider(dep_prov.instance_id) - if provider.type == ProviderType.PLAYER: - # mark all players of this provider as unavailable - for player in provider.players: - player.available = False - self.players.update(player.player_id) - try: - await provider.unload() - except Exception as err: - LOGGER.warning("Error while unload provider %s: %s", provider.name, str(err)) - finally: - self._providers.pop(instance_id, None) - await self._update_available_providers_cache() - self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers()) - - def _register_api_commands(self) -> None: - """Register all methods decorated as api_command within a class(instance).""" - for cls in ( - self, - self.config, - self.metadata, - self.music, - self.players, - self.player_queues, - ): - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - obj = getattr(cls, attr_name) - if hasattr(obj, "api_cmd"): - # method is decorated with our api decorator - self.register_api_command(obj.api_cmd, obj) - - async def _load_providers(self) -> None: - """Load providers from config.""" - # create default config for any 'builtin' providers (e.g. URL provider) - for prov_manifest in self._provider_manifests.values(): - if not prov_manifest.builtin: - continue - await self.config.create_builtin_provider_config(prov_manifest.domain) - - # load all configured (and enabled) providers - prov_configs = await self.config.get_provider_configs(include_values=True) - for prov_conf in prov_configs: - if not prov_conf.enabled: - continue - # Use a task so we can load multiple providers at once. - # If a provider fails, that will not block the loading of other providers. - self.create_task(self.load_provider(prov_conf.instance_id, allow_retry=True)) - - async def _load_provider(self, conf: ProviderConfig) -> None: - """Load (or reload) a provider.""" - # if provider is already loaded, stop and unload it first - await self.unload_provider(conf.instance_id) - LOGGER.debug("Loading provider %s", conf.name or conf.domain) - if not conf.enabled: - msg = "Provider is disabled" - raise SetupFailedError(msg) - - # validate config - try: - conf.validate() - except (KeyError, ValueError, AttributeError, TypeError) as err: - msg = "Configuration is invalid" - raise SetupFailedError(msg) from err - - domain = conf.domain - prov_manifest = self._provider_manifests.get(domain) - # check for other instances of this provider - existing = next((x for x in self.providers if x.domain == domain), None) - if existing and not prov_manifest.multi_instance: - msg = f"Provider {domain} already loaded and only one instance allowed." - raise SetupFailedError(msg) - # check valid manifest (just in case) - if not prov_manifest: - msg = f"Provider {domain} manifest not found" - raise SetupFailedError(msg) - - # handle dependency on other provider - if prov_manifest.depends_on and not self.get_provider(prov_manifest.depends_on): - # we can safely ignore this completely as the setup will be retried later - # automatically when the dependency is loaded - return - - # try to setup the module - prov_mod = await load_provider_module(domain, prov_manifest.requirements) - try: - async with asyncio.timeout(30): - provider = await prov_mod.setup(self, prov_manifest, conf) - except TimeoutError as err: - msg = f"Provider {domain} did not load within 30 seconds" - raise SetupFailedError(msg) from err - - self._providers[provider.instance_id] = provider - # run async setup - await provider.handle_async_init() - - # if we reach this point, the provider loaded successfully - LOGGER.info( - "Loaded %s provider %s", - provider.type.value, - conf.name or conf.domain, - ) - provider.available = True - - self.create_task(provider.loaded_in_mass()) - self.config.set(f"{CONF_PROVIDERS}/{conf.instance_id}/last_error", None) - self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers()) - await self._update_available_providers_cache() - - async def __load_provider_manifests(self) -> None: - """Preload all available provider manifest files.""" - - async def load_provider_manifest(provider_domain: str, provider_path: str) -> None: - """Preload all available provider manifest files.""" - # get files in subdirectory - for file_str in os.listdir(provider_path): - file_path = os.path.join(provider_path, file_str) - if not await isfile(file_path): - continue - if file_str != "manifest.json": - continue - try: - provider_manifest: ProviderManifest = await ProviderManifest.parse(file_path) - # check for icon.svg file - if not provider_manifest.icon_svg: - icon_path = os.path.join(provider_path, "icon.svg") - if await isfile(icon_path): - provider_manifest.icon_svg = await get_icon_string(icon_path) - # check for dark_icon file - if not provider_manifest.icon_svg_dark: - icon_path = os.path.join(provider_path, "icon_dark.svg") - if await isfile(icon_path): - provider_manifest.icon_svg_dark = await get_icon_string(icon_path) - self._provider_manifests[provider_manifest.domain] = provider_manifest - LOGGER.debug("Loaded manifest for provider %s", provider_manifest.name) - except Exception as exc: - LOGGER.exception( - "Error while loading manifest for provider %s", - provider_domain, - exc_info=exc, - ) - - async with TaskManager(self) as tg: - for dir_str in os.listdir(PROVIDERS_PATH): - if dir_str.startswith(("_", ".")): - continue - dir_path = os.path.join(PROVIDERS_PATH, dir_str) - if dir_str == "test" and not ENABLE_DEBUG: - continue - if not await isdir(dir_path): - continue - tg.create_task(load_provider_manifest(dir_str, dir_path)) - - async def _setup_discovery(self) -> None: - """Handle setup of MDNS discovery.""" - # create a global mdns browser - all_types: set[str] = set() - for prov_manifest in self._provider_manifests.values(): - if prov_manifest.mdns_discovery: - all_types.update(prov_manifest.mdns_discovery) - self._aiobrowser = AsyncServiceBrowser( - self.aiozc.zeroconf, - list(all_types), - handlers=[self._on_mdns_service_state_change], - ) - # register MA itself on mdns to be discovered - zeroconf_type = "_mass._tcp.local." - server_id = self.server_id - LOGGER.debug("Starting Zeroconf broadcast...") - info = AsyncServiceInfo( - zeroconf_type, - name=f"{server_id}.{zeroconf_type}", - addresses=[await get_ip_pton(self.webserver.publish_ip)], - port=self.webserver.publish_port, - properties=self.get_server_info().to_dict(), - server="mass.local.", - ) - try: - existing = getattr(self, "mass_zc_service_set", None) - if existing: - await self.aiozc.async_update_service(info) - else: - await self.aiozc.async_register_service(info) - self.mass_zc_service_set = True - except NonUniqueNameException: - LOGGER.error( - "Music Assistant instance with identical name present in the local network!" - ) - - def _on_mdns_service_state_change( - self, - zeroconf: Zeroconf, - service_type: str, - name: str, - state_change: ServiceStateChange, - ) -> None: - """Handle MDNS service state callback.""" - - async def process_mdns_state_change(prov: ProviderInstanceType): - if state_change == ServiceStateChange.Removed: - info = None - else: - info = AsyncServiceInfo(service_type, name) - await info.async_request(zeroconf, 3000) - await prov.on_mdns_service_state_change(name, state_change, info) - - LOGGER.log( - VERBOSE_LOG_LEVEL, - "Service %s of type %s state changed: %s", - name, - service_type, - state_change, - ) - for prov in self._providers.values(): - if not prov.manifest.mdns_discovery: - continue - if not prov.available: - continue - if service_type in prov.manifest.mdns_discovery: - self.create_task(process_mdns_state_change(prov)) - - async def __aenter__(self) -> Self: - """Return Context manager.""" - await self.start() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - await self.stop() - - async def _update_available_providers_cache(self) -> None: - """Update the global cache variable of loaded/available providers.""" - await set_global_cache_values( - { - "provider_domains": {x.domain for x in self.providers}, - "provider_instance_ids": {x.instance_id for x in self.providers}, - "available_providers": { - *{x.domain for x in self.providers}, - *{x.instance_id for x in self.providers}, - }, - "unique_providers": {x.lookup_key for x in self.providers}, - "streaming_providers": { - x.lookup_key - for x in self.providers - if x.type == ProviderType.MUSIC and x.is_streaming_provider - }, - "non_streaming_providers": { - x.lookup_key - for x in self.providers - if not (x.type == ProviderType.MUSIC and x.is_streaming_provider) - }, - } - ) diff --git a/mypy.ini b/mypy.ini index b2bdf56c..aa3c6247 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,4 +21,4 @@ disallow_untyped_decorators = true disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -packages=tests,music_assistant.client,music_assistant.common,music_assistant.server.providers.builtin,music_assistant.server.providers.filesystem_local,music_assistant.server.providers.filesystem_smb,music_assistant.server.providers.fully_kiosk,music_assistant.server.providers.jellyfin,music_assistant.server.providers.plex,music_assistant.server.providers.radiobrowser,music_assistant.server.providers.test,music_assistant.server.providers.theaudiodb,music_assistant.server.providers.tidal +packages=tests,music_assistant.providers.builtin,music_assistant.providers.filesystem_local,music_assistant.providers.filesystem_smb,music_assistant.providers.fully_kiosk,music_assistant.providers.jellyfin,music_assistant.providers.plex,music_assistant.providers.radiobrowser,music_assistant.providers.test,music_assistant.providers.theaudiodb,music_assistant.providers.tidal diff --git a/pyproject.toml b/pyproject.toml index 3fb50c92..9d841ba5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,38 +9,38 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = ["aiohttp", "orjson", "mashumaro"] -description = "Music Assistant" -license = {text = "Apache-2.0"} -readme = "README.md" -requires-python = ">=3.11" -version = "0.0.0" - -[project.optional-dependencies] -server = [ - "faust-cchardet>=2.1.18", +dependencies = [ "aiodns>=3.0.0", "Brotli>=1.0.9", "aiohttp==3.10.10", "aiofiles==24.1.0", "aiorun==2024.8.1", + "aiosqlite==0.20.0", "certifi==2024.8.30", "colorlog==6.8.2", - "aiosqlite==0.20.0", + "cryptography==43.0.3", "eyeD3==0.9.7", - "python-slugify==8.0.4", + "faust-cchardet>=2.1.18", + "ifaddr==0.2.0", "mashumaro==3.14", "memory-tempfile==2.2.3", "music-assistant-frontend==v2.9.14", + "music-assistant-models==1.0.3", + "orjson==3.10.7", "pillow==11.0.0", + "python-slugify==8.0.4", "unidecode==1.3.8", "xmltodict==0.14.2", - "orjson==3.10.10", "shortuuid==1.0.13", - "zeroconf==0.136.0", - "cryptography==43.0.3", - "ifaddr==0.2.0", + "zeroconf==0.135.0", ] +description = "Music Assistant" +license = {text = "Apache-2.0"} +readme = "README.md" +requires-python = ">=3.11" +version = "0.0.0" + +[project.optional-dependencies] test = [ "codespell==2.3.0", "isort==5.13.2", diff --git a/requirements_all.txt b/requirements_all.txt index ecb2fd6b..1ebea1f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,8 @@ ifaddr==0.2.0 mashumaro==3.14 memory-tempfile==2.2.3 music-assistant-frontend==v2.9.14 -orjson==3.10.10 +music-assistant-models==1.0.3 +orjson==3.10.7 pillow==11.0.0 pkce==1.0.3 plexapi==4.15.16 @@ -46,4 +47,4 @@ xmltodict==0.14.2 yt-dlp==2024.10.7 yt-dlp-youtube-accesstoken==0.1.1 ytmusicapi==1.8.1 -zeroconf==0.136.0 +zeroconf==0.135.0 diff --git a/scripts/gen_requirements_all.py b/scripts/gen_requirements_all.py index f10f4581..cff09664 100644 --- a/scripts/gen_requirements_all.py +++ b/scripts/gen_requirements_all.py @@ -19,17 +19,13 @@ def gather_core_requirements() -> list[str]: """Gather core requirements out of pyproject.toml.""" with open("pyproject.toml", "rb") as fp: data = tomllib.load(fp) - # server deps - dependencies: list[str] = data["project"]["optional-dependencies"]["server"] - # regular/client deps - dependencies += data["project"]["dependencies"] - return dependencies + return data["project"]["dependencies"] def gather_requirements_from_manifests() -> list[str]: """Gather all of the requirements from provider manifests.""" dependencies: list[str] = [] - providers_path = "music_assistant/server/providers" + providers_path = "music_assistant/providers" for dir_str in os.listdir(providers_path): dir_path = os.path.join(providers_path, dir_str) if not os.path.isdir(dir_path): diff --git a/scripts/setup.sh b/scripts/setup.sh index 53f161a1..1cff7a17 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -21,6 +21,6 @@ echo "Installing development dependencies..." pip install --upgrade pip pip install --upgrade uv -uv pip install -e ".[server]" +uv pip install -e "." uv pip install -e ".[test]" pre-commit install diff --git a/tests/__init__.py b/tests/__init__.py index 0a066f8c..326fb7be 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for Music Assistant go here.""" +"""Tests for the Music Assistant server.""" diff --git a/tests/common.py b/tests/common.py index 394540b2..a6ee1005 100644 --- a/tests/common.py +++ b/tests/common.py @@ -6,10 +6,10 @@ import pathlib from collections.abc import AsyncGenerator import aiofiles.os +from music_assistant_models.enums import EventType +from music_assistant_models.event import MassEvent -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.event import MassEvent -from music_assistant.server.server import MusicAssistant +from music_assistant import MusicAssistant def _get_fixture_folder(provider: str | None = None) -> pathlib.Path: diff --git a/tests/conftest.py b/tests/conftest.py index fc25b096..62c294cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator import pytest -from music_assistant.server.server import MusicAssistant +from music_assistant import MusicAssistant from tests.common import wait_for_sync_completion diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 00000000..c062c1d4 --- /dev/null +++ b/tests/core/__init__.py @@ -0,0 +1 @@ +"""Tests for the Music Assistant server core logic.""" diff --git a/tests/core/test_compare.py b/tests/core/test_compare.py new file mode 100644 index 00000000..8c25f7ab --- /dev/null +++ b/tests/core/test_compare.py @@ -0,0 +1,370 @@ +"""Tests for mediaitem compare helper functions.""" + +from music_assistant_models import media_items + +from music_assistant.helpers import compare + + +def test_compare_version() -> None: + """Test the version compare helper.""" + assert compare.compare_version("Remaster", "remaster") is True + assert compare.compare_version("Remastered", "remaster") is True + assert compare.compare_version("Remaster", "") is False + assert compare.compare_version("Remaster", "Remix") is False + assert compare.compare_version("", "Deluxe") is False + assert compare.compare_version("", "Live") is False + assert compare.compare_version("Live", "live") is True + assert compare.compare_version("Live", "live version") is True + assert compare.compare_version("Live version", "live") is True + assert compare.compare_version("Deluxe Edition", "Deluxe") is True + assert compare.compare_version("Deluxe Karaoke Edition", "Deluxe") is False + assert compare.compare_version("Deluxe Karaoke Edition", "Karaoke") is False + assert compare.compare_version("Deluxe Edition", "Edition Deluxe") is True + assert compare.compare_version("", "Karaoke Version") is False + assert compare.compare_version("Karaoke", "Karaoke Version") is True + assert compare.compare_version("Remaster", "Remaster Edition Deluxe") is False + assert compare.compare_version("Remastered Version", "Deluxe Version") is False + + +def test_compare_artist() -> None: + """Test artist comparison.""" + artist_a = media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + artist_b = media_items.Artist( + item_id="1", + provider="test2", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="2", provider_domain="test", provider_instance="test2" + ) + }, + ) + # test match on name match + assert compare.compare_artist(artist_a, artist_b) is True + # test match on name mismatch + artist_b.name = "Artist B" + assert compare.compare_artist(artist_a, artist_b) is False + # test on exact item_id match + artist_b.item_id = artist_a.item_id + artist_b.provider = artist_a.provider + assert compare.compare_artist(artist_a, artist_b) is True + # test on external id match + artist_b.name = "Artist B" + artist_b.item_id = "2" + artist_b.provider = "test2" + artist_a.external_ids = {(media_items.ExternalID.MB_ARTIST, "123")} + artist_b.external_ids = artist_a.external_ids + assert compare.compare_artist(artist_a, artist_b) is True + # test on external id mismatch + artist_b.name = artist_a.name + artist_b.external_ids = {(media_items.ExternalID.MB_ARTIST, "1234")} + assert compare.compare_artist(artist_a, artist_b) is False + # test on external id mismatch while name matches + artist_a = media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + external_ids={(media_items.ExternalID.MB_ARTIST, "123")}, + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + artist_b = media_items.Artist( + item_id="1", + provider="test2", + name="Artist A", + external_ids={(media_items.ExternalID.MB_ARTIST, "abc")}, + provider_mappings={ + media_items.ProviderMapping( + item_id="2", provider_domain="test", provider_instance="test2" + ) + }, + ) + assert compare.compare_artist(artist_a, artist_b) is False + + +def test_compare_album() -> None: + """Test album comparison.""" + album_a = media_items.Album( + item_id="1", + provider="test1", + name="Album A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + artists=media_items.UniqueList( + [ + media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + ] + ), + ) + album_b = media_items.Album( + item_id="1", + provider="test2", + name="Album A", + provider_mappings={ + media_items.ProviderMapping( + item_id="2", provider_domain="test", provider_instance="test2" + ) + }, + artists=media_items.UniqueList( + [ + media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + ] + ), + ) + # test match on name match + assert compare.compare_album(album_a, album_b) is True + # test match on name mismatch + album_b.name = "Album B" + assert compare.compare_album(album_a, album_b) is False + # test on version mismatch + album_b.name = album_a.name + album_b.version = "Deluxe" + assert compare.compare_album(album_a, album_b) is False + album_b.version = "Remix" + assert compare.compare_album(album_a, album_b) is False + # test on version match + album_b.name = album_a.name + album_a.version = "Deluxe" + album_b.version = "Deluxe Edition" + assert compare.compare_album(album_a, album_b) is True + # test on exact item_id match + album_b.item_id = album_a.item_id + album_b.provider = album_a.provider + assert compare.compare_album(album_a, album_b) is True + # test on external id match + album_b.name = "Album B" + album_b.item_id = "2" + album_b.provider = "test2" + album_a.external_ids = {(media_items.ExternalID.MB_ALBUM, "123")} + album_b.external_ids = album_a.external_ids + assert compare.compare_album(album_a, album_b) is True + # test on external id mismatch + album_b.name = album_a.name + album_b.external_ids = {(media_items.ExternalID.MB_ALBUM, "1234")} + assert compare.compare_album(album_a, album_b) is False + album_a.external_ids = set() + album_b.external_ids = set() + # fail on year mismatch + album_b.external_ids = set() + album_a.year = 2021 + album_b.year = 2020 + assert compare.compare_album(album_a, album_b) is False + # pass on year match + album_b.year = 2021 + assert compare.compare_album(album_a, album_b) is True + # fail on artist mismatch + album_a.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] + ) + album_b.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")] + ) + assert compare.compare_album(album_a, album_b) is False + # pass on partial artist match (if first artist matches) + album_a.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] + ) + album_b.artists = media_items.UniqueList( + [ + media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), + media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), + ] + ) + assert compare.compare_album(album_a, album_b) is True + # fail on partial artist match in strict mode + album_b.artists = media_items.UniqueList( + [ + media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), + media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), + ] + ) + assert compare.compare_album(album_a, album_b) is False + # partial artist match is allowed in non-strict mode + assert compare.compare_album(album_a, album_b, False) is True + + +def test_compare_track() -> None: # noqa: PLR0915 + """Test track comparison.""" + track_a = media_items.Track( + item_id="1", + provider="test1", + name="Track A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + artists=media_items.UniqueList( + [ + media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + ] + ), + ) + track_b = media_items.Track( + item_id="1", + provider="test2", + name="Track A", + provider_mappings={ + media_items.ProviderMapping( + item_id="2", provider_domain="test", provider_instance="test2" + ) + }, + artists=media_items.UniqueList( + [ + media_items.Artist( + item_id="1", + provider="test1", + name="Artist A", + provider_mappings={ + media_items.ProviderMapping( + item_id="1", provider_domain="test", provider_instance="test1" + ) + }, + ) + ] + ), + ) + # test match on name match + assert compare.compare_track(track_a, track_b) is True + # test match on name mismatch + track_b.name = "Track B" + assert compare.compare_track(track_a, track_b) is False + # test on version mismatch + track_b.name = track_a.name + track_b.version = "Deluxe" + assert compare.compare_track(track_a, track_b) is False + track_b.version = "Remix" + assert compare.compare_track(track_a, track_b) is False + # test on version mismatch + track_b.name = track_a.name + track_a.version = "" + track_b.version = "Remaster" + assert compare.compare_track(track_a, track_b) is False + track_b.version = "Remix" + assert compare.compare_track(track_a, track_b) is False + # test on version match + track_b.name = track_a.name + track_a.version = "Deluxe" + track_b.version = "Deluxe Edition" + assert compare.compare_track(track_a, track_b) is True + # test on exact item_id match + track_b.item_id = track_a.item_id + track_b.provider = track_a.provider + assert compare.compare_track(track_a, track_b) is True + # test on external id match + track_b.name = "Track B" + track_b.item_id = "2" + track_b.provider = "test2" + track_a.external_ids = {(media_items.ExternalID.MB_RECORDING, "123")} + track_b.external_ids = track_a.external_ids + assert compare.compare_track(track_a, track_b) is True + # test on external id mismatch + track_b.name = track_a.name + track_b.external_ids = {(media_items.ExternalID.MB_RECORDING, "1234")} + assert compare.compare_track(track_a, track_b) is False + track_a.external_ids = set() + track_b.external_ids = set() + # fail on artist mismatch + track_a.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] + ) + track_b.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")] + ) + assert compare.compare_track(track_a, track_b) is False + # pass on partial artist match (if first artist matches) + track_a.artists = media_items.UniqueList( + [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] + ) + track_b.artists = media_items.UniqueList( + [ + media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), + media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), + ] + ) + assert compare.compare_track(track_a, track_b) is True + # fail on partial artist match in strict mode + track_b.artists = media_items.UniqueList( + [ + media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), + media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), + ] + ) + assert compare.compare_track(track_a, track_b) is False + # partial artist match is allowed in non-strict mode + assert compare.compare_track(track_a, track_b, False) is True + track_b.artists = track_a.artists + # fail on album mismatch + track_a.album = media_items.ItemMapping(item_id="1", provider="test1", name="Album A") + track_b.album = media_items.ItemMapping(item_id="2", provider="test1", name="Album B") + assert compare.compare_track(track_a, track_b) is False + # pass on exact album(track) match (regardless duration) + track_b.album = track_a.album + track_a.disc_number = 1 + track_a.track_number = 1 + track_b.disc_number = track_a.disc_number + track_b.track_number = track_a.track_number + track_a.duration = 300 + track_b.duration = 310 + assert compare.compare_track(track_a, track_b) is True + # pass on album(track) mismatch + track_b.album = track_a.album + track_a.disc_number = 1 + track_a.track_number = 1 + track_b.disc_number = track_a.disc_number + track_b.track_number = 2 + track_b.duration = track_a.duration + assert compare.compare_track(track_a, track_b) is False + # test special case - ISRC match but MusicBrainz ID mismatch + # this can happen for some classical music albums + track_a.external_ids = { + (media_items.ExternalID.ISRC, "123"), + (media_items.ExternalID.MB_RECORDING, "abc"), + } + track_b.external_ids = { + (media_items.ExternalID.ISRC, "123"), + (media_items.ExternalID.MB_RECORDING, "abcd"), + } + assert compare.compare_track(track_a, track_b) is False diff --git a/tests/core/test_helpers.py b/tests/core/test_helpers.py new file mode 100644 index 00000000..b77fdd5a --- /dev/null +++ b/tests/core/test_helpers.py @@ -0,0 +1,82 @@ +"""Tests for utility/helper functions.""" + +import pytest +from music_assistant_models import media_items +from music_assistant_models.errors import MusicAssistantError + +from music_assistant.helpers import uri, util + + +def test_version_extract() -> None: + """Test the extraction of version from title.""" + test_str = "Bam Bam (feat. Ed Sheeran)" + title, version = util.parse_title_and_version(test_str) + assert title == "Bam Bam" + assert version == "" + test_str = "Bam Bam (feat. Ed Sheeran) - Karaoke Version" + title, version = util.parse_title_and_version(test_str) + assert title == "Bam Bam" + assert version == "Karaoke Version" + test_str = "Bam Bam (feat. Ed Sheeran) [Karaoke Version]" + title, version = util.parse_title_and_version(test_str) + assert title == "Bam Bam" + assert version == "Karaoke Version" + test_str = "SuperSong (2011 Remaster)" + title, version = util.parse_title_and_version(test_str) + assert title == "SuperSong" + assert version == "2011 Remaster" + test_str = "SuperSong (Live at Wembley)" + title, version = util.parse_title_and_version(test_str) + assert title == "SuperSong" + assert version == "Live at Wembley" + test_str = "SuperSong (Instrumental)" + title, version = util.parse_title_and_version(test_str) + assert title == "SuperSong" + assert version == "Instrumental" + test_str = "SuperSong (Explicit)" + title, version = util.parse_title_and_version(test_str) + assert title == "SuperSong" + assert version == "" + + +async def test_uri_parsing() -> None: + """Test parsing of URI.""" + # test regular uri + test_uri = "spotify://track/123456789" + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.TRACK + assert provider == "spotify" + assert item_id == "123456789" + # test spotify uri + test_uri = "spotify:track:123456789" + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.TRACK + assert provider == "spotify" + assert item_id == "123456789" + # test public play/open url + test_uri = "https://open.spotify.com/playlist/5lH9NjOeJvctAO92ZrKQNB?si=04a63c8234ac413e" + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.PLAYLIST + assert provider == "spotify" + assert item_id == "5lH9NjOeJvctAO92ZrKQNB" + # test filename with slashes as item_id + test_uri = "filesystem://track/Artist/Album/Track.flac" + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.TRACK + assert provider == "filesystem" + assert item_id == "Artist/Album/Track.flac" + # test regular url to builtin provider + test_uri = "http://radiostream.io/stream.mp3" + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.UNKNOWN + assert provider == "builtin" + assert item_id == "http://radiostream.io/stream.mp3" + # test local file to builtin provider + test_uri = __file__ + media_type, provider, item_id = await uri.parse_uri(test_uri) + assert media_type == media_items.MediaType.UNKNOWN + assert provider == "builtin" + assert item_id == __file__ + # test invalid uri + with pytest.raises(MusicAssistantError): + await uri.parse_uri("invalid://blah") diff --git a/tests/core/test_radio_stream_title.py b/tests/core/test_radio_stream_title.py new file mode 100644 index 00000000..b76d9122 --- /dev/null +++ b/tests/core/test_radio_stream_title.py @@ -0,0 +1,71 @@ +"""Tests for cleaning radio streamtitle.""" + +from music_assistant.helpers.util import clean_stream_title + + +def test_cleaning_streamtitle() -> None: + """Tests for cleaning radio streamtitle.""" + tstm = "Thirty Seconds To Mars - Closer to the Edge" + advert = "Advert" + + line = "Advertisement_Start_Length=00:00:29.960" + stream_title = clean_stream_title(line) + assert stream_title == advert + + line = "Advertisement_Stop" + stream_title = clean_stream_title(line) + assert stream_title == advert + + line = "START_AD_BREAK_6000" + stream_title = clean_stream_title(line) + assert stream_title == advert + + line = "STOP ADBREAK 1" + stream_title = clean_stream_title(line) + assert stream_title == advert + + line = "AD 2" + stream_title = clean_stream_title(line) + assert stream_title == advert + + line = 'title="Thirty Seconds To Mars - Closer to the Edge",artist="Thirty Seconds To Mars - Closer to the Edge",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' # noqa: E501 + stream_title = clean_stream_title(line) + assert stream_title == tstm + + line = 'title="https://listenapi.planetradio.co.uk/api9.2/eventdata/247801912",url="https://listenapi.planetradio.co.uk/api9.2/eventdata/247801912"' + stream_title = clean_stream_title(line) + assert stream_title == "" + + line = 'title="Thirty Seconds To Mars - Closer to the Edge https://nowplaying.scahw.com.au/",artist="Thirty Seconds To Mars - Closer to the Edge",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' # noqa: E501 + stream_title = clean_stream_title(line) + assert stream_title == tstm + + line = 'title="Closer to the Edge",artist="Thirty Seconds To Mars",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' + stream_title = clean_stream_title(line) + assert stream_title == tstm + + line = 'title="Thirty Seconds To Mars - Closer to the Edge"' + stream_title = clean_stream_title(line) + assert stream_title == tstm + + line = "Thirty Seconds To Mars - Closer to the Edge https://nowplaying.scahw.com.au/" + stream_title = clean_stream_title(line) + assert stream_title == tstm + + line = "Lonely Street By: Andy Williams - WALMRadio.com" + stream_title = clean_stream_title(line) + assert stream_title == "Andy Williams - Lonely Street" + + line = "Bye Bye Blackbird By: Sammy Davis Jr. - WALMRadio.com" + stream_title = clean_stream_title(line) + assert stream_title == "Sammy Davis Jr. - Bye Bye Blackbird" + + line = ( + "Asha Bhosle, Mohd Rafi (mp3yaar.com) - Gunguna Rahe Hain Bhanwre - Araadhna (mp3yaar.com)" + ) + stream_title = clean_stream_title(line) + assert stream_title == "Asha Bhosle, Mohd Rafi - Gunguna Rahe Hain Bhanwre - Araadhna" + + line = "Mohammed Rafi(Jatt.fm) - Rang Aur Noor Ki Baraat (Ghazal)(Jatt.fm)" + stream_title = clean_stream_title(line) + assert stream_title == "Mohammed Rafi - Rang Aur Noor Ki Baraat (Ghazal)" diff --git a/tests/core/test_server_base.py b/tests/core/test_server_base.py new file mode 100644 index 00000000..d4086787 --- /dev/null +++ b/tests/core/test_server_base.py @@ -0,0 +1,51 @@ +"""Tests for the core Music Assistant server object.""" + +import asyncio + +from music_assistant_models.enums import EventType +from music_assistant_models.event import MassEvent + +from music_assistant import MusicAssistant + + +async def test_start_and_stop_server(mass: MusicAssistant) -> None: + """Test that music assistant starts and stops cleanly.""" + domains = frozenset(p.domain for p in mass.get_provider_manifests()) + core_providers = frozenset( + ("builtin", "cache", "metadata", "music", "player_queues", "players", "streams") + ) + assert domains.issuperset(core_providers) + + +async def test_events(mass: MusicAssistant) -> None: + """Test that events sent by signal_event can be seen by subscribe.""" + filters: list[tuple[EventType | tuple[EventType, ...] | None, str | tuple[str, ...] | None]] = [ + (None, None), + (EventType.UNKNOWN, None), + ((EventType.UNKNOWN, EventType.AUTH_SESSION), None), + (None, "myid1"), + (None, ("myid1", "myid2")), + (EventType.UNKNOWN, "myid1"), + ] + + for event_filter, id_filter in filters: + flag = False + + def _ev(event: MassEvent) -> None: + assert event.event == EventType.UNKNOWN + assert event.data == "mytestdata" + assert event.object_id == "myid1" + nonlocal flag + flag = True + + remove_cb = mass.subscribe(_ev, event_filter, id_filter) + + mass.signal_event(EventType.UNKNOWN, "myid1", "mytestdata") + await asyncio.sleep(0) + assert flag is True + + flag = False + remove_cb() + mass.signal_event(EventType.UNKNOWN) + await asyncio.sleep(0) + assert flag is False diff --git a/tests/core/test_tags.py b/tests/core/test_tags.py new file mode 100644 index 00000000..fea52e30 --- /dev/null +++ b/tests/core/test_tags.py @@ -0,0 +1,75 @@ +"""Tests for parsing ID3 tags functions.""" + +import pathlib + +from music_assistant.helpers import tags + +RESOURCES_DIR = pathlib.Path(__file__).parent.resolve().joinpath("fixtures") + +FILE_1 = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3")) + + +async def test_parse_metadata_from_id3tags() -> None: + """Test parsing of parsing metadata from ID3 tags.""" + filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3")) + _tags = await tags.parse_tags(filename) + assert _tags.album == "MyAlbum" + assert _tags.title == "MyTitle" + assert _tags.duration == 1.032 + assert _tags.album_artists == ("MyArtist",) + assert _tags.artists == ("MyArtist", "MyArtist2") + assert _tags.genres == ("Genre1", "Genre2") + assert _tags.musicbrainz_albumartistids == ("abcdefg",) + assert _tags.musicbrainz_artistids == ("abcdefg",) + assert _tags.musicbrainz_releasegroupid == "abcdefg" + assert _tags.musicbrainz_recordingid == "abcdefg" + # test parsing disc/track number + _tags.tags["disc"] = "" + assert _tags.disc is None + _tags.tags["disc"] = "1" + assert _tags.disc == 1 + _tags.tags["disc"] = "1/1" + assert _tags.disc == 1 + # test parsing album year + _tags.tags["date"] = "blah" + assert _tags.year is None + _tags.tags.pop("date", None) + assert _tags.year is None + _tags.tags["date"] = "2022" + assert _tags.year == 2022 + _tags.tags["date"] = "2022-05-05" + assert _tags.year == 2022 + _tags.tags["date"] = "" + assert _tags.year is None + + +async def test_parse_metadata_from_filename() -> None: + """Test parsing of parsing metadata from filename.""" + filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle without Tags.mp3")) + _tags = await tags.parse_tags(filename) + assert _tags.album is None + assert _tags.title == "MyTitle without Tags" + assert _tags.duration == 1.032 + assert _tags.album_artists == () + assert _tags.artists == ("MyArtist",) + assert _tags.genres == () + assert _tags.musicbrainz_albumartistids == () + assert _tags.musicbrainz_artistids == () + assert _tags.musicbrainz_releasegroupid is None + assert _tags.musicbrainz_recordingid is None + + +async def test_parse_metadata_from_invalid_filename() -> None: + """Test parsing of parsing metadata from (invalid) filename.""" + filename = str(RESOURCES_DIR.joinpath("test.mp3")) + _tags = await tags.parse_tags(filename) + assert _tags.album is None + assert _tags.title == "test" + assert _tags.duration == 1.032 + assert _tags.album_artists == () + assert _tags.artists == (tags.UNKNOWN_ARTIST,) + assert _tags.genres == () + assert _tags.musicbrainz_albumartistids == () + assert _tags.musicbrainz_artistids == () + assert _tags.musicbrainz_releasegroupid is None + assert _tags.musicbrainz_recordingid is None diff --git a/tests/providers/filesystem/__init__.py b/tests/providers/filesystem/__init__.py new file mode 100644 index 00000000..8803e351 --- /dev/null +++ b/tests/providers/filesystem/__init__.py @@ -0,0 +1 @@ +"""Tests for Filesystem provider.""" diff --git a/tests/providers/filesystem/test_helpers.py b/tests/providers/filesystem/test_helpers.py new file mode 100644 index 00000000..591a36ec --- /dev/null +++ b/tests/providers/filesystem/test_helpers.py @@ -0,0 +1,91 @@ +"""Tests for utility/helper functions.""" + +import pytest + +from music_assistant.providers.filesystem_local import helpers + +# ruff: noqa: S108 + + +def test_get_artist_dir() -> None: + """Test the extraction of an artist dir.""" + album_path = "/tmp/Artist/Album" + artist_name = "Artist" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Artist" + album_path = "/tmp/artist/Album" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/artist" + album_path = "/tmp/Album" + assert helpers.get_artist_dir(artist_name, album_path) is None + album_path = "/tmp/ARTIST!/Album" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/ARTIST!" + album_path = "/tmp/Artist/Album" + artist_name = "Artist!" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Artist" + album_path = "/tmp/REM/Album" + artist_name = "R.E.M." + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/REM" + album_path = "/tmp/ACDC/Album" + artist_name = "AC/DC" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/ACDC" + album_path = "/tmp/Celine Dion/Album" + artist_name = "Céline Dion" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Celine Dion" + album_path = "/tmp/Antonin Dvorak/Album" + artist_name = "Antonín Dvořák" + assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Antonin Dvorak" + + +@pytest.mark.parametrize( + ("album_name", "track_dir", "expected"), + [ + # Test literal match + ( + "Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", + ), + # Test artist - album format + ( + "Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92", + ), + # Test artist - album (version) format + ( + "Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered)", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered)", + ), + # Test artist - album (version) format + ( + "Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered) - WEB", + "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered) - WEB", + ), + # Test album (version) format + ( + "Selected Ambient Works 85-92", + "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92 (Remastered)", + "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92 (Remastered)", + ), + # Test album name in dir + ( + "Selected Ambient Works 85-92", + "/home/user/Music/RandomDirWithSelected Ambient Works 85-92InIt", + "/home/user/Music/RandomDirWithSelected Ambient Works 85-92InIt", + ), + # Test no match + ( + "NonExistentAlbumName", + "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", + None, + ), + # Test empty album name + ("", "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", None), + # Test empty track dir + ("Selected Ambient Works 85-92", "", None), + ], +) +def test_get_album_dir(album_name: str, track_dir: str, expected: str) -> None: + """Test the extraction of an album dir.""" + assert helpers.get_album_dir(track_dir, album_name) == expected diff --git a/tests/providers/jellyfin/__init__.py b/tests/providers/jellyfin/__init__.py new file mode 100644 index 00000000..64080a61 --- /dev/null +++ b/tests/providers/jellyfin/__init__.py @@ -0,0 +1 @@ +"""Tests for Jellyfin provider.""" diff --git a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr new file mode 100644 index 00000000..69fe8a10 --- /dev/null +++ b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr @@ -0,0 +1,576 @@ +# serializer version: 1 +# name: test_parse_albums[infest] + dict({ + 'album_type': 'unknown', + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': 'e439648e08ade14e27d5de48fa97c88e', + 'media_type': 'artist', + 'name': 'Papa Roach', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'papa roach', + 'uri': 'xx-instance-id-xx://artist/e439648e08ade14e27d5de48fa97c88e', + 'version': '', + }), + ]), + 'external_ids': list([ + list([ + 'musicbrainz_albumid', + 'bf25b030-0cbb-495a-8d79-6c7fee20a089', + ]), + list([ + 'musicbrainz_releasegroupid', + '0193355a-cdfb-3936-afd2-44d651eb006d', + ]), + ]), + 'favorite': False, + 'item_id': '70b7288088b42d318f75dbcc41fd0091', + 'media_type': 'album', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'http://localhost:1234/Items/70b7288088b42d318f75dbcc41fd0091/Images/Primary?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'Infest', + 'position': None, + 'provider': 'jellyfin', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': '70b7288088b42d318f75dbcc41fd0091', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'sort_name': 'infest', + 'uri': 'jellyfin://album/70b7288088b42d318f75dbcc41fd0091', + 'version': '', + 'year': 2000, + }) +# --- +# name: test_parse_albums[this_is_christmas] + dict({ + 'album_type': 'unknown', + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '555b36f7d310d1b7405557a8775c6878', + 'media_type': 'artist', + 'name': 'Emmy the Great & Tim Wheeler', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'emmy the great & tim wheeler', + 'uri': 'xx-instance-id-xx://artist/555b36f7d310d1b7405557a8775c6878', + 'version': '', + }), + ]), + 'external_ids': list([ + list([ + 'musicbrainz_albumid', + 'b13a174d-527d-44a1-b8f8-a4c78b03b7d9', + ]), + list([ + 'musicbrainz_releasegroupid', + 'f002d6b7-17af-4f9e-8d30-5486548ffe6f', + ]), + ]), + 'favorite': False, + 'item_id': '32ed6a0091733dcff57eae67010f3d4b', + 'media_type': 'album', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'http://localhost:1234/Items/32ed6a0091733dcff57eae67010f3d4b/Images/Primary?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'This Is Christmas', + 'position': None, + 'provider': 'jellyfin', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': '32ed6a0091733dcff57eae67010f3d4b', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'sort_name': 'this is christmas', + 'uri': 'jellyfin://album/32ed6a0091733dcff57eae67010f3d4b', + 'version': '', + 'year': 2011, + }) +# --- +# name: test_parse_albums[yesterday_when_i_was_mad] + dict({ + 'album_type': 'unknown', + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '[unknown]', + 'media_type': 'artist', + 'name': '[unknown]', + 'provider': 'jellyfin', + 'sort_name': '[unknown]', + 'uri': 'jellyfin://artist/[unknown]', + 'version': '', + }), + ]), + 'external_ids': list([ + ]), + 'favorite': False, + 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', + 'media_type': 'album', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'Yesterday, When I Was Mad [Disc 2]', + 'position': None, + 'provider': 'jellyfin', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'sort_name': 'yesterday when i was mad [disc 0000000002]', + 'uri': 'jellyfin://album/7c8d0bd55291c7fc0451d17ebef30017', + 'version': '', + 'year': None, + }) +# --- +# name: test_parse_artists[ash] + dict({ + 'external_ids': list([ + list([ + 'musicbrainz_artistid', + '99164692-c02d-407c-81c9-25d338dd21f4', + ]), + ]), + 'favorite': False, + 'item_id': 'dd954bbf54398e247d803186d3585b79', + 'media_type': 'artist', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Backdrop/0?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'fanart', + }), + dict({ + 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Primary?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + dict({ + 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Logo?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'logo', + }), + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'Ash', + 'position': None, + 'provider': 'jellyfin', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': 'dd954bbf54398e247d803186d3585b79', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'sort_name': 'ash', + 'uri': 'jellyfin://artist/dd954bbf54398e247d803186d3585b79', + 'version': '', + }) +# --- +# name: test_parse_tracks[thrown_away] + dict({ + 'album': dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '70b7288088b42d318f75dbcc41fd0091', + 'media_type': 'album', + 'name': 'Unknown Album (70b7288088b42d318f75dbcc41fd0091)', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'unknown album (70b7288088b42d318f75dbcc41fd0091)', + 'uri': 'xx-instance-id-xx://album/70b7288088b42d318f75dbcc41fd0091', + 'version': '', + }), + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '[unknown]', + 'media_type': 'artist', + 'name': '[unknown]', + 'provider': 'jellyfin', + 'sort_name': '[unknown]', + 'uri': 'jellyfin://artist/[unknown]', + 'version': '', + }), + ]), + 'disc_number': 0, + 'duration': 577, + 'external_ids': list([ + ]), + 'favorite': False, + 'item_id': 'b5319fb11cde39fca2023184fcfa9862', + 'media_type': 'track', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': '11 Thrown Away', + 'position': 0, + 'provider': 'xx-instance-id-xx', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': 'mp3', + 'output_format_str': 'mp3', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': 'b5319fb11cde39fca2023184fcfa9862', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': 'http://localhost:1234/Audio/b5319fb11cde39fca2023184fcfa9862/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', + }), + ]), + 'sort_name': '11 thrown away', + 'track_number': 0, + 'uri': 'xx-instance-id-xx://track/b5319fb11cde39fca2023184fcfa9862', + 'version': '', + }) +# --- +# name: test_parse_tracks[where_the_bands_are] + dict({ + 'album': None, + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '94875b0dd58cbf5245a135982133651a', + 'media_type': 'artist', + 'name': 'Dead Like Harry', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'dead like harry', + 'uri': 'xx-instance-id-xx://artist/94875b0dd58cbf5245a135982133651a', + 'version': '', + }), + ]), + 'disc_number': 1, + 'duration': 246, + 'external_ids': list([ + ]), + 'favorite': False, + 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', + 'media_type': 'track', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'http://localhost:1234/Items/54918f75ee8f6c8b8dc5efd680644f29/Images/Primary?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'Where the Bands Are (2018 Version)', + 'position': 1, + 'provider': 'xx-instance-id-xx', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': 'aac', + 'output_format_str': 'aac', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': 'http://localhost:1234/Audio/54918f75ee8f6c8b8dc5efd680644f29/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', + }), + ]), + 'sort_name': 'where the bands are (2018 version)', + 'track_number': 1, + 'uri': 'xx-instance-id-xx://track/54918f75ee8f6c8b8dc5efd680644f29', + 'version': '', + }) +# --- +# name: test_parse_tracks[zombie_christmas] + dict({ + 'album': dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '32ed6a0091733dcff57eae67010f3d4b', + 'media_type': 'album', + 'name': 'This Is Christmas', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'this is christmas', + 'uri': 'xx-instance-id-xx://album/32ed6a0091733dcff57eae67010f3d4b', + 'version': '', + }), + 'artists': list([ + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': 'a0c459294295710546c81c20a8d9abfc', + 'media_type': 'artist', + 'name': 'Emmy the Great', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'emmy the great', + 'uri': 'xx-instance-id-xx://artist/a0c459294295710546c81c20a8d9abfc', + 'version': '', + }), + dict({ + 'available': True, + 'external_ids': list([ + ]), + 'image': None, + 'item_id': '1952db245ddef4e41dcd016475379190', + 'media_type': 'artist', + 'name': 'Tim Wheeler', + 'provider': 'xx-instance-id-xx', + 'sort_name': 'tim wheeler', + 'uri': 'xx-instance-id-xx://artist/1952db245ddef4e41dcd016475379190', + 'version': '', + }), + ]), + 'disc_number': 1, + 'duration': 224, + 'external_ids': list([ + list([ + 'musicbrainz_recordingid', + '17d1019d-d4f4-326c-b4bb-d8aec2607bd7', + ]), + ]), + 'favorite': False, + 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', + 'media_type': 'track', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': None, + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'http://localhost:1234/Items/fb12a77f49616a9fc56a6fab2b94174c/Images/Primary?api_key=ACCESS_TOKEN', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'last_refresh': None, + 'links': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'review': None, + 'style': None, + }), + 'name': 'Zombie Christmas', + 'position': 8, + 'provider': 'xx-instance-id-xx', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'content_type': 'aac', + 'output_format_str': 'aac', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', + 'provider_domain': 'jellyfin', + 'provider_instance': 'xx-instance-id-xx', + 'url': 'http://localhost:1234/Audio/fb12a77f49616a9fc56a6fab2b94174c/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', + }), + ]), + 'sort_name': 'zombie christmas', + 'track_number': 8, + 'uri': 'xx-instance-id-xx://track/fb12a77f49616a9fc56a6fab2b94174c', + 'version': '', + }) +# --- diff --git a/tests/providers/jellyfin/fixtures/albums/infest.json b/tests/providers/jellyfin/fixtures/albums/infest.json new file mode 100644 index 00000000..8c45b7db --- /dev/null +++ b/tests/providers/jellyfin/fixtures/albums/infest.json @@ -0,0 +1,105 @@ +{ + "Name": "Infest", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "70b7288088b42d318f75dbcc41fd0091", + "Etag": "ecf97edd78eb2b76a30ae2adba6b66e5", + "DateCreated": "2023-12-11T12:10:40.7607527Z", + "CanDelete": false, + "CanDownload": false, + "SortName": "infest", + "PremiereDate": "2000-04-25T00:00:00.0000000Z", + "ExternalUrls": [ + { + "Name": "MusicBrainz", + "Url": "https://musicbrainz.org/release/bf25b030-0cbb-495a-8d79-6c7fee20a089" + }, + { + "Name": "MusicBrainz", + "Url": "https://musicbrainz.org/release-group/0193355a-cdfb-3936-afd2-44d651eb006d" + } + ], + "Path": "/media/music/Papa Roach/Infest", + "EnableMediaSourceDisplay": true, + "ChannelId": null, + "Taglines": [], + "Genres": [ + "Alt Metal" + ], + "CumulativeRunTimeTicks": 27614273019, + "RunTimeTicks": 27614273019, + "PlayAccess": "Full", + "ProductionYear": 2000, + "RemoteTrailers": [], + "ProviderIds": { + "MusicBrainzAlbum": "bf25b030-0cbb-495a-8d79-6c7fee20a089", + "MusicBrainzReleaseGroup": "0193355a-cdfb-3936-afd2-44d651eb006d", + "MusicBrainzAlbumArtist": "c5eb9407-caeb-4303-b383-6929aa94021c" + }, + "IsFolder": true, + "ParentId": "e439648e08ade14e27d5de48fa97c88e", + "Type": "MusicAlbum", + "People": [], + "Studios": [], + "GenreItems": [ + { + "Name": "Alt Metal", + "Id": "7fae6ce8290515d5dfedc4e1894c1522" + } + ], + "ParentLogoItemId": "e439648e08ade14e27d5de48fa97c88e", + "ParentBackdropItemId": "e439648e08ade14e27d5de48fa97c88e", + "ParentBackdropImageTags": [ + "c3d584db117d4c2bba5a975f391a965e" + ], + "LocalTrailerCount": 0, + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "MusicAlbum-MusicBrainzReleaseGroup-0193355a-cdfb-3936-afd2-44d651eb006d" + }, + "RecursiveItemCount": 11, + "ChildCount": 11, + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "f13d7f51d4f1f8b6fcd620855eb88c1e", + "Tags": [], + "PrimaryImageAspectRatio": 1, + "Artists": [ + "Papa Roach" + ], + "ArtistItems": [ + { + "Name": "Papa Roach", + "Id": "e439648e08ade14e27d5de48fa97c88e" + } + ], + "AlbumArtist": "Papa Roach", + "AlbumArtists": [ + { + "Name": "Papa Roach", + "Id": "e439648e08ade14e27d5de48fa97c88e" + } + ], + "ImageTags": { + "Primary": "bcbe1ac159b0522743c9a0fe5401f948" + }, + "BackdropImageTags": [], + "ParentLogoImageTag": "d58ea3bfadfb34e66033f55b8b2198c4", + "ImageBlurHashes": { + "Primary": { + "bcbe1ac159b0522743c9a0fe5401f948": "ecQb^8vf.S_2xY*0%hxDV[kXyYx^IUNGxt=ZsSNGV@njxuxuaKayS2" + }, + "Logo": { + "d58ea3bfadfb34e66033f55b8b2198c4": "OQBftnWXD%WBNHoft7xaWBaej[fkoLay0Lax-:ofxZazRj" + }, + "Backdrop": { + "c3d584db117d4c2bba5a975f391a965e": "W%F~5FodtRNGkCt6~Woet8Rkazs:-;j@ofoLWBkCxuWBays:axof" + } + }, + "LocationType": "FileSystem", + "MediaType": "Unknown", + "LockedFields": [], + "LockData": false, + "NormalizationGain": -11 +} diff --git a/tests/providers/jellyfin/fixtures/albums/this_is_christmas.json b/tests/providers/jellyfin/fixtures/albums/this_is_christmas.json new file mode 100644 index 00000000..30a77430 --- /dev/null +++ b/tests/providers/jellyfin/fixtures/albums/this_is_christmas.json @@ -0,0 +1,57 @@ +{ + "Name": "This Is Christmas", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "32ed6a0091733dcff57eae67010f3d4b", + "SortName": "this is christmas", + "PremiereDate": "2011-11-21T00:00:00.0000000Z", + "ChannelId": null, + "RunTimeTicks": 18722017229, + "ProductionYear": 2011, + "ProviderIds": { + "MusicBrainzAlbum": "b13a174d-527d-44a1-b8f8-a4c78b03b7d9", + "MusicBrainzReleaseGroup": "f002d6b7-17af-4f9e-8d30-5486548ffe6f", + "MusicBrainzAlbumArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0" + }, + "IsFolder": true, + "Type": "MusicAlbum", + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "MusicAlbum-MusicBrainzReleaseGroup-f002d6b7-17af-4f9e-8d30-5486548ffe6f" + }, + "Artists": [ + "Emmy the Great", + "Tim Wheeler" + ], + "ArtistItems": [ + { + "Name": "Emmy the Great", + "Id": "a0c459294295710546c81c20a8d9abfc" + }, + { + "Name": "Tim Wheeler", + "Id": "1952db245ddef4e41dcd016475379190" + } + ], + "AlbumArtist": "Emmy the Great & Tim Wheeler", + "AlbumArtists": [ + { + "Name": "Emmy the Great & Tim Wheeler", + "Id": "555b36f7d310d1b7405557a8775c6878" + } + ], + "ImageTags": { + "Primary": "b685ba2b9247aca1ea66dda557bb8f54" + }, + "BackdropImageTags": [], + "ImageBlurHashes": { + "Primary": { + "b685ba2b9247aca1ea66dda557bb8f54": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC" + } + }, + "LocationType": "FileSystem", + "MediaType": "Unknown", + "NormalizationGain": -12.3 +} diff --git a/tests/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json b/tests/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json new file mode 100644 index 00000000..c63b49da --- /dev/null +++ b/tests/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json @@ -0,0 +1,39 @@ +{ + "Name": "Yesterday, When I Was Mad [Disc 2]", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "7c8d0bd55291c7fc0451d17ebef30017", + "SortName": "yesterday when i was mad [disc 0000000002]", + "ChannelId": null, + "RunTimeTicks": 0, + "ProviderIds": {}, + "IsFolder": true, + "Type": "MusicAlbum", + "ParentLogoItemId": "87dff4e376665b79ff3fb0e3e69594e4", + "ParentBackdropItemId": "87dff4e376665b79ff3fb0e3e69594e4", + "ParentBackdropImageTags": [ + "c8d58817f36f1a3337d14307e9b22ef3" + ], + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "7c8d0bd5-5291-c7fc-0451-d17ebef30017" + }, + "Artists": [], + "ArtistItems": [], + "AlbumArtists": [], + "ImageTags": {}, + "BackdropImageTags": [], + "ParentLogoImageTag": "ef313161af6195475d4ba26b245640b0", + "ImageBlurHashes": { + "Logo": { + "ef313161af6195475d4ba26b245640b0": "OmPGZ|R+Xlo{oNxve.x]4mNFbIf5s;t8t,tQDiM_tRoMbI" + }, + "Backdrop": { + "c8d58817f36f1a3337d14307e9b22ef3": "W$Pi;m?b_Noeofx]~CRjNvxuofozs;ofRjRjofof-;xuoyRjWBoJ" + } + }, + "LocationType": "FileSystem", + "MediaType": "Unknown" +} diff --git a/tests/providers/jellyfin/fixtures/artists/ash.json b/tests/providers/jellyfin/fixtures/artists/ash.json new file mode 100644 index 00000000..8df9ddb3 --- /dev/null +++ b/tests/providers/jellyfin/fixtures/artists/ash.json @@ -0,0 +1,40 @@ +{ + "Name": "Ash", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "dd954bbf54398e247d803186d3585b79", + "SortName": "ash", + "ChannelId": null, + "RunTimeTicks": 509234691363, + "ProviderIds": { + "MusicBrainzArtist": "99164692-c02d-407c-81c9-25d338dd21f4" + }, + "IsFolder": true, + "Type": "MusicArtist", + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "Artist-Musicbrainz-99164692-c02d-407c-81c9-25d338dd21f4" + }, + "ImageTags": { + "Primary": "8a543e58fda6d2f374263a4dcd0d2fbd", + "Logo": "662f82868ad2964190daea171f3fcf08" + }, + "BackdropImageTags": [ + "8a4c3c67629b28673de7af433a1efd68" + ], + "ImageBlurHashes": { + "Backdrop": { + "8a4c3c67629b28673de7af433a1efd68": "WOE2-2Tdahs=KjwJ?w%2NGkBoMkCE%n+j?jErqNwo#Nwsmo1oejF" + }, + "Primary": { + "8a543e58fda6d2f374263a4dcd0d2fbd": "eNHd?IWD4;~qI;#5~U-;D*-:^fxUM{xa%K-;RkIW%MWA4:Si%Mngn}" + }, + "Logo": { + "662f82868ad2964190daea171f3fcf08": "OXD]o8j[00WBt7t7ayofWBWBt7ofRjayM{ofofRjRjj[t7" + } + }, + "LocationType": "FileSystem", + "MediaType": "Unknown" +} diff --git a/tests/providers/jellyfin/fixtures/tracks/thrown_away.json b/tests/providers/jellyfin/fixtures/tracks/thrown_away.json new file mode 100644 index 00000000..1f3c9f0f --- /dev/null +++ b/tests/providers/jellyfin/fixtures/tracks/thrown_away.json @@ -0,0 +1,131 @@ +{ + "Name": "11 Thrown Away", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "b5319fb11cde39fca2023184fcfa9862", + "CanDownload": true, + "HasLyrics": false, + "Container": "mp3", + "SortName": "0000 - 0000 - 11 Thrown Away", + "MediaSources": [ + { + "Protocol": "File", + "Id": "b5319fb11cde39fca2023184fcfa9862", + "Path": "/media/music/Papa Roach/Infest/11 Thrown Away.m4a", + "Type": "Default", + "Container": "mp3", + "Size": 11283443, + "Name": "11 Thrown Away", + "IsRemote": false, + "ETag": "d76cb3d88267e21a9a5a7b43e5981c99", + "RunTimeTicks": 5777763270, + "ReadAtNativeFramerate": false, + "IgnoreDts": false, + "IgnoreIndex": false, + "GenPtsInput": false, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": false, + "RequiresOpening": false, + "RequiresClosing": false, + "RequiresLooping": false, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "mp3", + "TimeBase": "1/14112000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "MP3 - Stereo", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 156231, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 156232, + "RequiredHttpHeaders": {}, + "TranscodingSubProtocol": "http", + "DefaultAudioStreamIndex": 0 + } + ], + "ChannelId": null, + "RunTimeTicks": 5777763270, + "IndexNumber": 0, + "ParentIndexNumber": 0, + "ProviderIds": {}, + "IsFolder": false, + "Type": "Audio", + "ParentLogoItemId": "e439648e08ade14e27d5de48fa97c88e", + "ParentBackdropItemId": "e439648e08ade14e27d5de48fa97c88e", + "ParentBackdropImageTags": [ + "c3d584db117d4c2bba5a975f391a965e" + ], + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "0000-000011 Thrown Away" + }, + "Artists": [], + "ArtistItems": [], + "AlbumId": "70b7288088b42d318f75dbcc41fd0091", + "AlbumPrimaryImageTag": "bcbe1ac159b0522743c9a0fe5401f948", + "AlbumArtists": [], + "MediaStreams": [ + { + "Codec": "mp3", + "TimeBase": "1/14112000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "MP3 - Stereo", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 156231, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "ImageTags": {}, + "BackdropImageTags": [], + "ParentLogoImageTag": "d58ea3bfadfb34e66033f55b8b2198c4", + "ImageBlurHashes": { + "Logo": { + "d58ea3bfadfb34e66033f55b8b2198c4": "OQBftnWXD%WBNHoft7xaWBaej[fkoLay0Lax-:ofxZazRj" + }, + "Backdrop": { + "c3d584db117d4c2bba5a975f391a965e": "W%F~5FodtRNGkCt6~Woet8Rkazs:-;j@ofoLWBkCxuWBays:axof" + }, + "Primary": { + "bcbe1ac159b0522743c9a0fe5401f948": "ecQb^8vf.S_2xY*0%hxDV[kXyYx^IUNGxt=ZsSNGV@njxuxuaKayS2" + } + }, + "LocationType": "FileSystem", + "MediaType": "Audio" +} diff --git a/tests/providers/jellyfin/fixtures/tracks/where_the_bands_are.json b/tests/providers/jellyfin/fixtures/tracks/where_the_bands_are.json new file mode 100644 index 00000000..1403c9d8 --- /dev/null +++ b/tests/providers/jellyfin/fixtures/tracks/where_the_bands_are.json @@ -0,0 +1,198 @@ +{ + "Name": "Where the Bands Are (2018 Version)", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "54918f75ee8f6c8b8dc5efd680644f29", + "CanDownload": true, + "HasLyrics": false, + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "SortName": "0001 - 0001 - Where the Bands Are (2018 Version)", + "PremiereDate": "2018-01-01T00:00:00.0000000Z", + "MediaSources": [ + { + "Protocol": "File", + "Id": "54918f75ee8f6c8b8dc5efd680644f29", + "Path": "/media/music/Dead Like Harry/01 Where the Bands Are (2018 Version).m4a", + "Type": "Default", + "Container": "m4a", + "Size": 9167268, + "Name": "01 Where the Bands Are (2018 Version)", + "IsRemote": false, + "ETag": "7a60d53d522c32d2659150c99f0b8ed6", + "RunTimeTicks": 2464333790, + "ReadAtNativeFramerate": false, + "IgnoreDts": false, + "IgnoreIndex": false, + "GenPtsInput": false, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": false, + "RequiresOpening": false, + "RequiresClosing": false, + "RequiresLooping": false, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/44100", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "English - AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 278038, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + }, + { + "Codec": "mjpeg", + "ColorSpace": "bt470bg", + "TimeBase": "1/90000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "IsInterlaced": false, + "IsAVC": false, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 600, + "Width": 600, + "RealFrameRate": 90000, + "Profile": "Baseline", + "Type": "EmbeddedImage", + "AspectRatio": "1:1", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuvj420p", + "Level": -99, + "IsAnamorphic": false + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 297598, + "RequiredHttpHeaders": {}, + "TranscodingSubProtocol": "http", + "DefaultAudioStreamIndex": 0 + } + ], + "ChannelId": null, + "RunTimeTicks": 2464333790, + "ProductionYear": 2018, + "IndexNumber": 1, + "ParentIndexNumber": 1, + "ProviderIds": {}, + "IsFolder": false, + "Type": "Audio", + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "Dead Like Harry-Where the Bands Are (2018 Version) - Single-0001-0001Where the Bands Are (2018 Version)" + }, + "Artists": [ + "Dead Like Harry" + ], + "ArtistItems": [ + { + "Name": "Dead Like Harry", + "Id": "94875b0dd58cbf5245a135982133651a" + } + ], + "Album": "Where the Bands Are (2018 Version) - Single", + "AlbumArtist": "Dead Like Harry", + "AlbumArtists": [ + { + "Name": "Dead Like Harry", + "Id": "94875b0dd58cbf5245a135982133651a" + } + ], + "MediaStreams": [ + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/44100", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "English - AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 278038, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + }, + { + "Codec": "mjpeg", + "ColorSpace": "bt470bg", + "TimeBase": "1/90000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "IsInterlaced": false, + "IsAVC": false, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 600, + "Width": 600, + "RealFrameRate": 90000, + "Profile": "Baseline", + "Type": "EmbeddedImage", + "AspectRatio": "1:1", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuvj420p", + "Level": -99, + "IsAnamorphic": false + } + ], + "ImageTags": { + "Primary": "dbd792d6c27313d01ed7c2dce85f785b" + }, + "BackdropImageTags": [], + "ImageBlurHashes": { + "Primary": { + "dbd792d6c27313d01ed7c2dce85f785b": "eXI|wC^*={t6-o_3o#o#oft7~WtRbwNHS5?bS$ozaeR-o}WXt7jYR+" + } + }, + "LocationType": "FileSystem", + "MediaType": "Audio", + "NormalizationGain": -10.7 +} diff --git a/tests/providers/jellyfin/fixtures/tracks/zombie_christmas.json b/tests/providers/jellyfin/fixtures/tracks/zombie_christmas.json new file mode 100644 index 00000000..24207dbc --- /dev/null +++ b/tests/providers/jellyfin/fixtures/tracks/zombie_christmas.json @@ -0,0 +1,212 @@ +{ + "Name": "Zombie Christmas", + "ServerId": "58f180d8d2b34927bcfd73eee400ffad", + "Id": "fb12a77f49616a9fc56a6fab2b94174c", + "CanDownload": true, + "HasLyrics": false, + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "SortName": "0001 - 0008 - Zombie Christmas", + "PremiereDate": "2011-11-21T00:00:00.0000000Z", + "MediaSources": [ + { + "Protocol": "File", + "Id": "fb12a77f49616a9fc56a6fab2b94174c", + "Path": "/media/music/Emmy the Great & Tim Wheeler/This Is Christmas/8. Zombie Christmas.m4a", + "Type": "Default", + "Container": "m4a", + "Size": 8225981, + "Name": "8. Zombie Christmas", + "IsRemote": false, + "ETag": "0185e75e1fdad95cb227ce8d815d8cb5", + "RunTimeTicks": 2249317010, + "ReadAtNativeFramerate": false, + "IgnoreDts": false, + "IgnoreIndex": false, + "GenPtsInput": false, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": false, + "RequiresOpening": false, + "RequiresClosing": false, + "RequiresLooping": false, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/44100", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "English - AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 267933, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + }, + { + "Codec": "mjpeg", + "ColorSpace": "bt470bg", + "TimeBase": "1/90000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "IsInterlaced": false, + "IsAVC": false, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 449, + "Width": 500, + "RealFrameRate": 90000, + "Profile": "Baseline", + "Type": "EmbeddedImage", + "AspectRatio": "500:449", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuvj420p", + "Level": -99, + "IsAnamorphic": false + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 292568, + "RequiredHttpHeaders": {}, + "TranscodingSubProtocol": "http", + "DefaultAudioStreamIndex": 0 + } + ], + "ChannelId": null, + "RunTimeTicks": 2249317010, + "ProductionYear": 2011, + "IndexNumber": 8, + "ParentIndexNumber": 1, + "ProviderIds": { + "MusicBrainzArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0", + "MusicBrainzAlbumArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0", + "MusicBrainzAlbum": "b13a174d-527d-44a1-b8f8-a4c78b03b7d9", + "MusicBrainzReleaseGroup": "f002d6b7-17af-4f9e-8d30-5486548ffe6f", + "MusicBrainzTrack": "17d1019d-d4f4-326c-b4bb-d8aec2607bd7" + }, + "IsFolder": false, + "Type": "Audio", + "UserData": { + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": false, + "Played": false, + "Key": "Emmy the Great & Tim Wheeler-This Is Christmas-0001-0008Zombie Christmas" + }, + "Artists": [ + "Emmy the Great", + "Tim Wheeler" + ], + "ArtistItems": [ + { + "Name": "Emmy the Great", + "Id": "a0c459294295710546c81c20a8d9abfc" + }, + { + "Name": "Tim Wheeler", + "Id": "1952db245ddef4e41dcd016475379190" + } + ], + "Album": "This Is Christmas", + "AlbumId": "32ed6a0091733dcff57eae67010f3d4b", + "AlbumPrimaryImageTag": "b685ba2b9247aca1ea66dda557bb8f54", + "AlbumArtist": "Emmy the Great & Tim Wheeler", + "AlbumArtists": [ + { + "Name": "Emmy the Great & Tim Wheeler", + "Id": "555b36f7d310d1b7405557a8775c6878" + } + ], + "MediaStreams": [ + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/44100", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "DisplayTitle": "English - AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 267933, + "Channels": 2, + "SampleRate": 44100, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + }, + { + "Codec": "mjpeg", + "ColorSpace": "bt470bg", + "TimeBase": "1/90000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "IsInterlaced": false, + "IsAVC": false, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": false, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 449, + "Width": 500, + "RealFrameRate": 90000, + "Profile": "Baseline", + "Type": "EmbeddedImage", + "AspectRatio": "500:449", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuvj420p", + "Level": -99, + "IsAnamorphic": false + } + ], + "ImageTags": { + "Primary": "c8e39ff125c3ba39a5791570dffa4b83" + }, + "BackdropImageTags": [], + "ImageBlurHashes": { + "Primary": { + "c8e39ff125c3ba39a5791570dffa4b83": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC", + "b685ba2b9247aca1ea66dda557bb8f54": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC" + } + }, + "LocationType": "FileSystem", + "MediaType": "Audio", + "NormalizationGain": -11.9 +} diff --git a/tests/providers/jellyfin/test_init.py b/tests/providers/jellyfin/test_init.py new file mode 100644 index 00000000..44e04eae --- /dev/null +++ b/tests/providers/jellyfin/test_init.py @@ -0,0 +1,53 @@ +"""Tests for the Jellyfin provider.""" + +from collections.abc import AsyncGenerator +from unittest import mock + +import pytest +from aiojellyfin.testing import FixtureBuilder +from music_assistant_models.config_entries import ProviderConfig + +from music_assistant import MusicAssistant +from tests.common import get_fixtures_dir, wait_for_sync_completion + + +@pytest.fixture +async def jellyfin_provider(mass: MusicAssistant) -> AsyncGenerator[ProviderConfig, None]: + """Configure an aiojellyfin test fixture, and add a provider to mass that uses it.""" + f = FixtureBuilder() + async for _, artist in get_fixtures_dir("artists", "jellyfin"): + f.add_json_bytes(artist) + + async for _, album in get_fixtures_dir("albums", "jellyfin"): + f.add_json_bytes(album) + + async for _, track in get_fixtures_dir("tracks", "jellyfin"): + f.add_json_bytes(track) + + authenticate_by_name = f.to_authenticate_by_name() + + with mock.patch(".providers.jellyfin.authenticate_by_name", authenticate_by_name): + async with wait_for_sync_completion(mass): + config = await mass.config.save_provider_config( + "jellyfin", + { + "url": "http://localhost", + "username": "username", + "password": "password", + }, + ) + + yield config + + +@pytest.mark.usefixtures("jellyfin_provider") +async def test_initial_sync(mass: MusicAssistant) -> None: + """Test that initial sync worked.""" + artists = await mass.music.artists.library_items(search="Ash") + assert artists[0].name == "Ash" + + albums = await mass.music.albums.library_items(search="christmas") + assert albums[0].name == "This Is Christmas" + + tracks = await mass.music.tracks.library_items(search="where the bands are") + assert tracks[0].name == "Where the Bands Are (2018 Version)" diff --git a/tests/providers/jellyfin/test_parsers.py b/tests/providers/jellyfin/test_parsers.py new file mode 100644 index 00000000..7952cae5 --- /dev/null +++ b/tests/providers/jellyfin/test_parsers.py @@ -0,0 +1,77 @@ +"""Test we can parse Jellyfin models into Music Assistant models.""" + +import logging +import pathlib +from collections.abc import AsyncGenerator + +import aiofiles +import aiohttp +import pytest +from aiojellyfin import Artist, Connection, SessionConfiguration +from mashumaro.codecs.json import JSONDecoder +from syrupy.assertion import SnapshotAssertion + +from music_assistant.providers.jellyfin.parsers import parse_album, parse_artist, parse_track + +FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" +ARTIST_FIXTURES = list(FIXTURES_DIR.glob("artists/*.json")) +ALBUM_FIXTURES = list(FIXTURES_DIR.glob("albums/*.json")) +TRACK_FIXTURES = list(FIXTURES_DIR.glob("tracks/*.json")) + +ARTIST_DECODER = JSONDecoder(Artist) + +_LOGGER = logging.getLogger(__name__) + + +@pytest.fixture +async def connection() -> AsyncGenerator[Connection, None]: + """Spin up a dummy connection.""" + async with aiohttp.ClientSession() as session: + session_config = SessionConfiguration( + session=session, + url="http://localhost:1234", + app_name="X", + app_version="0.0.0", + device_id="X", + device_name="localhost", + ) + yield Connection(session_config, "USER_ID", "ACCESS_TOKEN") + + +@pytest.mark.parametrize("example", ARTIST_FIXTURES, ids=lambda val: str(val.stem)) +async def test_parse_artists( + example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion +) -> None: + """Test we can parse artists.""" + async with aiofiles.open(example) as fp: + raw_data = ARTIST_DECODER.decode(await fp.read()) + parsed = parse_artist(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() + # sort external Ids to ensure they are always in the same order for snapshot testing + parsed["external_ids"].sort() + assert snapshot == parsed + + +@pytest.mark.parametrize("example", ALBUM_FIXTURES, ids=lambda val: str(val.stem)) +async def test_parse_albums( + example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion +) -> None: + """Test we can parse albums.""" + async with aiofiles.open(example) as fp: + raw_data = ARTIST_DECODER.decode(await fp.read()) + parsed = parse_album(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() + # sort external Ids to ensure they are always in the same order for snapshot testing + parsed["external_ids"].sort() + assert snapshot == parsed + + +@pytest.mark.parametrize("example", TRACK_FIXTURES, ids=lambda val: str(val.stem)) +async def test_parse_tracks( + example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion +) -> None: + """Test we can parse tracks.""" + async with aiofiles.open(example) as fp: + raw_data = ARTIST_DECODER.decode(await fp.read()) + parsed = parse_track(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() + # sort external Ids to ensure they are always in the same order for snapshot testing + parsed["external_ids"] + assert snapshot == parsed diff --git a/tests/server/__init__.py b/tests/server/__init__.py deleted file mode 100644 index 326fb7be..00000000 --- a/tests/server/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Music Assistant server.""" diff --git a/tests/server/providers/filesystem/__init__.py b/tests/server/providers/filesystem/__init__.py deleted file mode 100644 index 8803e351..00000000 --- a/tests/server/providers/filesystem/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Filesystem provider.""" diff --git a/tests/server/providers/filesystem/test_helpers.py b/tests/server/providers/filesystem/test_helpers.py deleted file mode 100644 index cac1bb78..00000000 --- a/tests/server/providers/filesystem/test_helpers.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Tests for utility/helper functions.""" - -import pytest - -from music_assistant.server.providers.filesystem_local import helpers - -# ruff: noqa: S108 - - -def test_get_artist_dir() -> None: - """Test the extraction of an artist dir.""" - album_path = "/tmp/Artist/Album" - artist_name = "Artist" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Artist" - album_path = "/tmp/artist/Album" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/artist" - album_path = "/tmp/Album" - assert helpers.get_artist_dir(artist_name, album_path) is None - album_path = "/tmp/ARTIST!/Album" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/ARTIST!" - album_path = "/tmp/Artist/Album" - artist_name = "Artist!" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Artist" - album_path = "/tmp/REM/Album" - artist_name = "R.E.M." - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/REM" - album_path = "/tmp/ACDC/Album" - artist_name = "AC/DC" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/ACDC" - album_path = "/tmp/Celine Dion/Album" - artist_name = "Céline Dion" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Celine Dion" - album_path = "/tmp/Antonin Dvorak/Album" - artist_name = "Antonín Dvořák" - assert helpers.get_artist_dir(artist_name, album_path) == "/tmp/Antonin Dvorak" - - -@pytest.mark.parametrize( - ("album_name", "track_dir", "expected"), - [ - # Test literal match - ( - "Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", - ), - # Test artist - album format - ( - "Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92", - ), - # Test artist - album (version) format - ( - "Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered)", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered)", - ), - # Test artist - album (version) format - ( - "Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered) - WEB", - "/home/user/Music/Aphex Twin - Selected Ambient Works 85-92 (Remastered) - WEB", - ), - # Test album (version) format - ( - "Selected Ambient Works 85-92", - "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92 (Remastered)", - "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92 (Remastered)", - ), - # Test album name in dir - ( - "Selected Ambient Works 85-92", - "/home/user/Music/RandomDirWithSelected Ambient Works 85-92InIt", - "/home/user/Music/RandomDirWithSelected Ambient Works 85-92InIt", - ), - # Test no match - ( - "NonExistentAlbumName", - "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", - None, - ), - # Test empty album name - ("", "/home/user/Music/Aphex Twin/Selected Ambient Works 85-92", None), - # Test empty track dir - ("Selected Ambient Works 85-92", "", None), - ], -) -def test_get_album_dir(album_name: str, track_dir: str, expected: str) -> None: - """Test the extraction of an album dir.""" - assert helpers.get_album_dir(track_dir, album_name) == expected diff --git a/tests/server/providers/jellyfin/__init__.py b/tests/server/providers/jellyfin/__init__.py deleted file mode 100644 index 64080a61..00000000 --- a/tests/server/providers/jellyfin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Jellyfin provider.""" diff --git a/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr deleted file mode 100644 index 69fe8a10..00000000 --- a/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr +++ /dev/null @@ -1,576 +0,0 @@ -# serializer version: 1 -# name: test_parse_albums[infest] - dict({ - 'album_type': 'unknown', - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': 'e439648e08ade14e27d5de48fa97c88e', - 'media_type': 'artist', - 'name': 'Papa Roach', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'papa roach', - 'uri': 'xx-instance-id-xx://artist/e439648e08ade14e27d5de48fa97c88e', - 'version': '', - }), - ]), - 'external_ids': list([ - list([ - 'musicbrainz_albumid', - 'bf25b030-0cbb-495a-8d79-6c7fee20a089', - ]), - list([ - 'musicbrainz_releasegroupid', - '0193355a-cdfb-3936-afd2-44d651eb006d', - ]), - ]), - 'favorite': False, - 'item_id': '70b7288088b42d318f75dbcc41fd0091', - 'media_type': 'album', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - dict({ - 'path': 'http://localhost:1234/Items/70b7288088b42d318f75dbcc41fd0091/Images/Primary?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'thumb', - }), - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'Infest', - 'position': None, - 'provider': 'jellyfin', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': '?', - 'output_format_str': '?', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': '70b7288088b42d318f75dbcc41fd0091', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': None, - }), - ]), - 'sort_name': 'infest', - 'uri': 'jellyfin://album/70b7288088b42d318f75dbcc41fd0091', - 'version': '', - 'year': 2000, - }) -# --- -# name: test_parse_albums[this_is_christmas] - dict({ - 'album_type': 'unknown', - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '555b36f7d310d1b7405557a8775c6878', - 'media_type': 'artist', - 'name': 'Emmy the Great & Tim Wheeler', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'emmy the great & tim wheeler', - 'uri': 'xx-instance-id-xx://artist/555b36f7d310d1b7405557a8775c6878', - 'version': '', - }), - ]), - 'external_ids': list([ - list([ - 'musicbrainz_albumid', - 'b13a174d-527d-44a1-b8f8-a4c78b03b7d9', - ]), - list([ - 'musicbrainz_releasegroupid', - 'f002d6b7-17af-4f9e-8d30-5486548ffe6f', - ]), - ]), - 'favorite': False, - 'item_id': '32ed6a0091733dcff57eae67010f3d4b', - 'media_type': 'album', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - dict({ - 'path': 'http://localhost:1234/Items/32ed6a0091733dcff57eae67010f3d4b/Images/Primary?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'thumb', - }), - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'This Is Christmas', - 'position': None, - 'provider': 'jellyfin', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': '?', - 'output_format_str': '?', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': '32ed6a0091733dcff57eae67010f3d4b', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': None, - }), - ]), - 'sort_name': 'this is christmas', - 'uri': 'jellyfin://album/32ed6a0091733dcff57eae67010f3d4b', - 'version': '', - 'year': 2011, - }) -# --- -# name: test_parse_albums[yesterday_when_i_was_mad] - dict({ - 'album_type': 'unknown', - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '[unknown]', - 'media_type': 'artist', - 'name': '[unknown]', - 'provider': 'jellyfin', - 'sort_name': '[unknown]', - 'uri': 'jellyfin://artist/[unknown]', - 'version': '', - }), - ]), - 'external_ids': list([ - ]), - 'favorite': False, - 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', - 'media_type': 'album', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'Yesterday, When I Was Mad [Disc 2]', - 'position': None, - 'provider': 'jellyfin', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': '?', - 'output_format_str': '?', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': None, - }), - ]), - 'sort_name': 'yesterday when i was mad [disc 0000000002]', - 'uri': 'jellyfin://album/7c8d0bd55291c7fc0451d17ebef30017', - 'version': '', - 'year': None, - }) -# --- -# name: test_parse_artists[ash] - dict({ - 'external_ids': list([ - list([ - 'musicbrainz_artistid', - '99164692-c02d-407c-81c9-25d338dd21f4', - ]), - ]), - 'favorite': False, - 'item_id': 'dd954bbf54398e247d803186d3585b79', - 'media_type': 'artist', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - dict({ - 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Backdrop/0?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'fanart', - }), - dict({ - 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Primary?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'thumb', - }), - dict({ - 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Logo?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'logo', - }), - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'Ash', - 'position': None, - 'provider': 'jellyfin', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': '?', - 'output_format_str': '?', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': 'dd954bbf54398e247d803186d3585b79', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': None, - }), - ]), - 'sort_name': 'ash', - 'uri': 'jellyfin://artist/dd954bbf54398e247d803186d3585b79', - 'version': '', - }) -# --- -# name: test_parse_tracks[thrown_away] - dict({ - 'album': dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '70b7288088b42d318f75dbcc41fd0091', - 'media_type': 'album', - 'name': 'Unknown Album (70b7288088b42d318f75dbcc41fd0091)', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'unknown album (70b7288088b42d318f75dbcc41fd0091)', - 'uri': 'xx-instance-id-xx://album/70b7288088b42d318f75dbcc41fd0091', - 'version': '', - }), - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '[unknown]', - 'media_type': 'artist', - 'name': '[unknown]', - 'provider': 'jellyfin', - 'sort_name': '[unknown]', - 'uri': 'jellyfin://artist/[unknown]', - 'version': '', - }), - ]), - 'disc_number': 0, - 'duration': 577, - 'external_ids': list([ - ]), - 'favorite': False, - 'item_id': 'b5319fb11cde39fca2023184fcfa9862', - 'media_type': 'track', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': '11 Thrown Away', - 'position': 0, - 'provider': 'xx-instance-id-xx', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': 'mp3', - 'output_format_str': 'mp3', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': 'b5319fb11cde39fca2023184fcfa9862', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': 'http://localhost:1234/Audio/b5319fb11cde39fca2023184fcfa9862/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', - }), - ]), - 'sort_name': '11 thrown away', - 'track_number': 0, - 'uri': 'xx-instance-id-xx://track/b5319fb11cde39fca2023184fcfa9862', - 'version': '', - }) -# --- -# name: test_parse_tracks[where_the_bands_are] - dict({ - 'album': None, - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '94875b0dd58cbf5245a135982133651a', - 'media_type': 'artist', - 'name': 'Dead Like Harry', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'dead like harry', - 'uri': 'xx-instance-id-xx://artist/94875b0dd58cbf5245a135982133651a', - 'version': '', - }), - ]), - 'disc_number': 1, - 'duration': 246, - 'external_ids': list([ - ]), - 'favorite': False, - 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', - 'media_type': 'track', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - dict({ - 'path': 'http://localhost:1234/Items/54918f75ee8f6c8b8dc5efd680644f29/Images/Primary?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'thumb', - }), - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'Where the Bands Are (2018 Version)', - 'position': 1, - 'provider': 'xx-instance-id-xx', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': 'aac', - 'output_format_str': 'aac', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': 'http://localhost:1234/Audio/54918f75ee8f6c8b8dc5efd680644f29/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', - }), - ]), - 'sort_name': 'where the bands are (2018 version)', - 'track_number': 1, - 'uri': 'xx-instance-id-xx://track/54918f75ee8f6c8b8dc5efd680644f29', - 'version': '', - }) -# --- -# name: test_parse_tracks[zombie_christmas] - dict({ - 'album': dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '32ed6a0091733dcff57eae67010f3d4b', - 'media_type': 'album', - 'name': 'This Is Christmas', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'this is christmas', - 'uri': 'xx-instance-id-xx://album/32ed6a0091733dcff57eae67010f3d4b', - 'version': '', - }), - 'artists': list([ - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': 'a0c459294295710546c81c20a8d9abfc', - 'media_type': 'artist', - 'name': 'Emmy the Great', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'emmy the great', - 'uri': 'xx-instance-id-xx://artist/a0c459294295710546c81c20a8d9abfc', - 'version': '', - }), - dict({ - 'available': True, - 'external_ids': list([ - ]), - 'image': None, - 'item_id': '1952db245ddef4e41dcd016475379190', - 'media_type': 'artist', - 'name': 'Tim Wheeler', - 'provider': 'xx-instance-id-xx', - 'sort_name': 'tim wheeler', - 'uri': 'xx-instance-id-xx://artist/1952db245ddef4e41dcd016475379190', - 'version': '', - }), - ]), - 'disc_number': 1, - 'duration': 224, - 'external_ids': list([ - list([ - 'musicbrainz_recordingid', - '17d1019d-d4f4-326c-b4bb-d8aec2607bd7', - ]), - ]), - 'favorite': False, - 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', - 'media_type': 'track', - 'metadata': dict({ - 'chapters': None, - 'copyright': None, - 'description': None, - 'explicit': None, - 'genres': None, - 'images': list([ - dict({ - 'path': 'http://localhost:1234/Items/fb12a77f49616a9fc56a6fab2b94174c/Images/Primary?api_key=ACCESS_TOKEN', - 'provider': 'xx-instance-id-xx', - 'remotely_accessible': False, - 'type': 'thumb', - }), - ]), - 'label': None, - 'last_refresh': None, - 'links': None, - 'lyrics': None, - 'mood': None, - 'performers': None, - 'popularity': None, - 'preview': None, - 'review': None, - 'style': None, - }), - 'name': 'Zombie Christmas', - 'position': 8, - 'provider': 'xx-instance-id-xx', - 'provider_mappings': list([ - dict({ - 'audio_format': dict({ - 'bit_depth': 16, - 'bit_rate': 0, - 'channels': 2, - 'content_type': 'aac', - 'output_format_str': 'aac', - 'sample_rate': 44100, - }), - 'available': True, - 'details': None, - 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', - 'provider_domain': 'jellyfin', - 'provider_instance': 'xx-instance-id-xx', - 'url': 'http://localhost:1234/Audio/fb12a77f49616a9fc56a6fab2b94174c/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN', - }), - ]), - 'sort_name': 'zombie christmas', - 'track_number': 8, - 'uri': 'xx-instance-id-xx://track/fb12a77f49616a9fc56a6fab2b94174c', - 'version': '', - }) -# --- diff --git a/tests/server/providers/jellyfin/fixtures/albums/infest.json b/tests/server/providers/jellyfin/fixtures/albums/infest.json deleted file mode 100644 index 8c45b7db..00000000 --- a/tests/server/providers/jellyfin/fixtures/albums/infest.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "Name": "Infest", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "70b7288088b42d318f75dbcc41fd0091", - "Etag": "ecf97edd78eb2b76a30ae2adba6b66e5", - "DateCreated": "2023-12-11T12:10:40.7607527Z", - "CanDelete": false, - "CanDownload": false, - "SortName": "infest", - "PremiereDate": "2000-04-25T00:00:00.0000000Z", - "ExternalUrls": [ - { - "Name": "MusicBrainz", - "Url": "https://musicbrainz.org/release/bf25b030-0cbb-495a-8d79-6c7fee20a089" - }, - { - "Name": "MusicBrainz", - "Url": "https://musicbrainz.org/release-group/0193355a-cdfb-3936-afd2-44d651eb006d" - } - ], - "Path": "/media/music/Papa Roach/Infest", - "EnableMediaSourceDisplay": true, - "ChannelId": null, - "Taglines": [], - "Genres": [ - "Alt Metal" - ], - "CumulativeRunTimeTicks": 27614273019, - "RunTimeTicks": 27614273019, - "PlayAccess": "Full", - "ProductionYear": 2000, - "RemoteTrailers": [], - "ProviderIds": { - "MusicBrainzAlbum": "bf25b030-0cbb-495a-8d79-6c7fee20a089", - "MusicBrainzReleaseGroup": "0193355a-cdfb-3936-afd2-44d651eb006d", - "MusicBrainzAlbumArtist": "c5eb9407-caeb-4303-b383-6929aa94021c" - }, - "IsFolder": true, - "ParentId": "e439648e08ade14e27d5de48fa97c88e", - "Type": "MusicAlbum", - "People": [], - "Studios": [], - "GenreItems": [ - { - "Name": "Alt Metal", - "Id": "7fae6ce8290515d5dfedc4e1894c1522" - } - ], - "ParentLogoItemId": "e439648e08ade14e27d5de48fa97c88e", - "ParentBackdropItemId": "e439648e08ade14e27d5de48fa97c88e", - "ParentBackdropImageTags": [ - "c3d584db117d4c2bba5a975f391a965e" - ], - "LocalTrailerCount": 0, - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "MusicAlbum-MusicBrainzReleaseGroup-0193355a-cdfb-3936-afd2-44d651eb006d" - }, - "RecursiveItemCount": 11, - "ChildCount": 11, - "SpecialFeatureCount": 0, - "DisplayPreferencesId": "f13d7f51d4f1f8b6fcd620855eb88c1e", - "Tags": [], - "PrimaryImageAspectRatio": 1, - "Artists": [ - "Papa Roach" - ], - "ArtistItems": [ - { - "Name": "Papa Roach", - "Id": "e439648e08ade14e27d5de48fa97c88e" - } - ], - "AlbumArtist": "Papa Roach", - "AlbumArtists": [ - { - "Name": "Papa Roach", - "Id": "e439648e08ade14e27d5de48fa97c88e" - } - ], - "ImageTags": { - "Primary": "bcbe1ac159b0522743c9a0fe5401f948" - }, - "BackdropImageTags": [], - "ParentLogoImageTag": "d58ea3bfadfb34e66033f55b8b2198c4", - "ImageBlurHashes": { - "Primary": { - "bcbe1ac159b0522743c9a0fe5401f948": "ecQb^8vf.S_2xY*0%hxDV[kXyYx^IUNGxt=ZsSNGV@njxuxuaKayS2" - }, - "Logo": { - "d58ea3bfadfb34e66033f55b8b2198c4": "OQBftnWXD%WBNHoft7xaWBaej[fkoLay0Lax-:ofxZazRj" - }, - "Backdrop": { - "c3d584db117d4c2bba5a975f391a965e": "W%F~5FodtRNGkCt6~Woet8Rkazs:-;j@ofoLWBkCxuWBays:axof" - } - }, - "LocationType": "FileSystem", - "MediaType": "Unknown", - "LockedFields": [], - "LockData": false, - "NormalizationGain": -11 -} diff --git a/tests/server/providers/jellyfin/fixtures/albums/this_is_christmas.json b/tests/server/providers/jellyfin/fixtures/albums/this_is_christmas.json deleted file mode 100644 index 30a77430..00000000 --- a/tests/server/providers/jellyfin/fixtures/albums/this_is_christmas.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "Name": "This Is Christmas", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "32ed6a0091733dcff57eae67010f3d4b", - "SortName": "this is christmas", - "PremiereDate": "2011-11-21T00:00:00.0000000Z", - "ChannelId": null, - "RunTimeTicks": 18722017229, - "ProductionYear": 2011, - "ProviderIds": { - "MusicBrainzAlbum": "b13a174d-527d-44a1-b8f8-a4c78b03b7d9", - "MusicBrainzReleaseGroup": "f002d6b7-17af-4f9e-8d30-5486548ffe6f", - "MusicBrainzAlbumArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0" - }, - "IsFolder": true, - "Type": "MusicAlbum", - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "MusicAlbum-MusicBrainzReleaseGroup-f002d6b7-17af-4f9e-8d30-5486548ffe6f" - }, - "Artists": [ - "Emmy the Great", - "Tim Wheeler" - ], - "ArtistItems": [ - { - "Name": "Emmy the Great", - "Id": "a0c459294295710546c81c20a8d9abfc" - }, - { - "Name": "Tim Wheeler", - "Id": "1952db245ddef4e41dcd016475379190" - } - ], - "AlbumArtist": "Emmy the Great & Tim Wheeler", - "AlbumArtists": [ - { - "Name": "Emmy the Great & Tim Wheeler", - "Id": "555b36f7d310d1b7405557a8775c6878" - } - ], - "ImageTags": { - "Primary": "b685ba2b9247aca1ea66dda557bb8f54" - }, - "BackdropImageTags": [], - "ImageBlurHashes": { - "Primary": { - "b685ba2b9247aca1ea66dda557bb8f54": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC" - } - }, - "LocationType": "FileSystem", - "MediaType": "Unknown", - "NormalizationGain": -12.3 -} diff --git a/tests/server/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json b/tests/server/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json deleted file mode 100644 index c63b49da..00000000 --- a/tests/server/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "Name": "Yesterday, When I Was Mad [Disc 2]", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "7c8d0bd55291c7fc0451d17ebef30017", - "SortName": "yesterday when i was mad [disc 0000000002]", - "ChannelId": null, - "RunTimeTicks": 0, - "ProviderIds": {}, - "IsFolder": true, - "Type": "MusicAlbum", - "ParentLogoItemId": "87dff4e376665b79ff3fb0e3e69594e4", - "ParentBackdropItemId": "87dff4e376665b79ff3fb0e3e69594e4", - "ParentBackdropImageTags": [ - "c8d58817f36f1a3337d14307e9b22ef3" - ], - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "7c8d0bd5-5291-c7fc-0451-d17ebef30017" - }, - "Artists": [], - "ArtistItems": [], - "AlbumArtists": [], - "ImageTags": {}, - "BackdropImageTags": [], - "ParentLogoImageTag": "ef313161af6195475d4ba26b245640b0", - "ImageBlurHashes": { - "Logo": { - "ef313161af6195475d4ba26b245640b0": "OmPGZ|R+Xlo{oNxve.x]4mNFbIf5s;t8t,tQDiM_tRoMbI" - }, - "Backdrop": { - "c8d58817f36f1a3337d14307e9b22ef3": "W$Pi;m?b_Noeofx]~CRjNvxuofozs;ofRjRjofof-;xuoyRjWBoJ" - } - }, - "LocationType": "FileSystem", - "MediaType": "Unknown" -} diff --git a/tests/server/providers/jellyfin/fixtures/artists/ash.json b/tests/server/providers/jellyfin/fixtures/artists/ash.json deleted file mode 100644 index 8df9ddb3..00000000 --- a/tests/server/providers/jellyfin/fixtures/artists/ash.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "Name": "Ash", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "dd954bbf54398e247d803186d3585b79", - "SortName": "ash", - "ChannelId": null, - "RunTimeTicks": 509234691363, - "ProviderIds": { - "MusicBrainzArtist": "99164692-c02d-407c-81c9-25d338dd21f4" - }, - "IsFolder": true, - "Type": "MusicArtist", - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "Artist-Musicbrainz-99164692-c02d-407c-81c9-25d338dd21f4" - }, - "ImageTags": { - "Primary": "8a543e58fda6d2f374263a4dcd0d2fbd", - "Logo": "662f82868ad2964190daea171f3fcf08" - }, - "BackdropImageTags": [ - "8a4c3c67629b28673de7af433a1efd68" - ], - "ImageBlurHashes": { - "Backdrop": { - "8a4c3c67629b28673de7af433a1efd68": "WOE2-2Tdahs=KjwJ?w%2NGkBoMkCE%n+j?jErqNwo#Nwsmo1oejF" - }, - "Primary": { - "8a543e58fda6d2f374263a4dcd0d2fbd": "eNHd?IWD4;~qI;#5~U-;D*-:^fxUM{xa%K-;RkIW%MWA4:Si%Mngn}" - }, - "Logo": { - "662f82868ad2964190daea171f3fcf08": "OXD]o8j[00WBt7t7ayofWBWBt7ofRjayM{ofofRjRjj[t7" - } - }, - "LocationType": "FileSystem", - "MediaType": "Unknown" -} diff --git a/tests/server/providers/jellyfin/fixtures/tracks/thrown_away.json b/tests/server/providers/jellyfin/fixtures/tracks/thrown_away.json deleted file mode 100644 index 1f3c9f0f..00000000 --- a/tests/server/providers/jellyfin/fixtures/tracks/thrown_away.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "Name": "11 Thrown Away", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "b5319fb11cde39fca2023184fcfa9862", - "CanDownload": true, - "HasLyrics": false, - "Container": "mp3", - "SortName": "0000 - 0000 - 11 Thrown Away", - "MediaSources": [ - { - "Protocol": "File", - "Id": "b5319fb11cde39fca2023184fcfa9862", - "Path": "/media/music/Papa Roach/Infest/11 Thrown Away.m4a", - "Type": "Default", - "Container": "mp3", - "Size": 11283443, - "Name": "11 Thrown Away", - "IsRemote": false, - "ETag": "d76cb3d88267e21a9a5a7b43e5981c99", - "RunTimeTicks": 5777763270, - "ReadAtNativeFramerate": false, - "IgnoreDts": false, - "IgnoreIndex": false, - "GenPtsInput": false, - "SupportsTranscoding": true, - "SupportsDirectStream": true, - "SupportsDirectPlay": true, - "IsInfiniteStream": false, - "RequiresOpening": false, - "RequiresClosing": false, - "RequiresLooping": false, - "SupportsProbing": true, - "MediaStreams": [ - { - "Codec": "mp3", - "TimeBase": "1/14112000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "MP3 - Stereo", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 156231, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - } - ], - "MediaAttachments": [], - "Formats": [], - "Bitrate": 156232, - "RequiredHttpHeaders": {}, - "TranscodingSubProtocol": "http", - "DefaultAudioStreamIndex": 0 - } - ], - "ChannelId": null, - "RunTimeTicks": 5777763270, - "IndexNumber": 0, - "ParentIndexNumber": 0, - "ProviderIds": {}, - "IsFolder": false, - "Type": "Audio", - "ParentLogoItemId": "e439648e08ade14e27d5de48fa97c88e", - "ParentBackdropItemId": "e439648e08ade14e27d5de48fa97c88e", - "ParentBackdropImageTags": [ - "c3d584db117d4c2bba5a975f391a965e" - ], - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "0000-000011 Thrown Away" - }, - "Artists": [], - "ArtistItems": [], - "AlbumId": "70b7288088b42d318f75dbcc41fd0091", - "AlbumPrimaryImageTag": "bcbe1ac159b0522743c9a0fe5401f948", - "AlbumArtists": [], - "MediaStreams": [ - { - "Codec": "mp3", - "TimeBase": "1/14112000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "MP3 - Stereo", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 156231, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - } - ], - "ImageTags": {}, - "BackdropImageTags": [], - "ParentLogoImageTag": "d58ea3bfadfb34e66033f55b8b2198c4", - "ImageBlurHashes": { - "Logo": { - "d58ea3bfadfb34e66033f55b8b2198c4": "OQBftnWXD%WBNHoft7xaWBaej[fkoLay0Lax-:ofxZazRj" - }, - "Backdrop": { - "c3d584db117d4c2bba5a975f391a965e": "W%F~5FodtRNGkCt6~Woet8Rkazs:-;j@ofoLWBkCxuWBays:axof" - }, - "Primary": { - "bcbe1ac159b0522743c9a0fe5401f948": "ecQb^8vf.S_2xY*0%hxDV[kXyYx^IUNGxt=ZsSNGV@njxuxuaKayS2" - } - }, - "LocationType": "FileSystem", - "MediaType": "Audio" -} diff --git a/tests/server/providers/jellyfin/fixtures/tracks/where_the_bands_are.json b/tests/server/providers/jellyfin/fixtures/tracks/where_the_bands_are.json deleted file mode 100644 index 1403c9d8..00000000 --- a/tests/server/providers/jellyfin/fixtures/tracks/where_the_bands_are.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "Name": "Where the Bands Are (2018 Version)", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "54918f75ee8f6c8b8dc5efd680644f29", - "CanDownload": true, - "HasLyrics": false, - "Container": "mov,mp4,m4a,3gp,3g2,mj2", - "SortName": "0001 - 0001 - Where the Bands Are (2018 Version)", - "PremiereDate": "2018-01-01T00:00:00.0000000Z", - "MediaSources": [ - { - "Protocol": "File", - "Id": "54918f75ee8f6c8b8dc5efd680644f29", - "Path": "/media/music/Dead Like Harry/01 Where the Bands Are (2018 Version).m4a", - "Type": "Default", - "Container": "m4a", - "Size": 9167268, - "Name": "01 Where the Bands Are (2018 Version)", - "IsRemote": false, - "ETag": "7a60d53d522c32d2659150c99f0b8ed6", - "RunTimeTicks": 2464333790, - "ReadAtNativeFramerate": false, - "IgnoreDts": false, - "IgnoreIndex": false, - "GenPtsInput": false, - "SupportsTranscoding": true, - "SupportsDirectStream": true, - "SupportsDirectPlay": true, - "IsInfiniteStream": false, - "RequiresOpening": false, - "RequiresClosing": false, - "RequiresLooping": false, - "SupportsProbing": true, - "MediaStreams": [ - { - "Codec": "aac", - "CodecTag": "mp4a", - "Language": "eng", - "TimeBase": "1/44100", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "English - AAC - Stereo - Default", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 278038, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": true, - "IsForced": false, - "IsHearingImpaired": false, - "Profile": "LC", - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - }, - { - "Codec": "mjpeg", - "ColorSpace": "bt470bg", - "TimeBase": "1/90000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "IsInterlaced": false, - "IsAVC": false, - "BitDepth": 8, - "RefFrames": 1, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Height": 600, - "Width": 600, - "RealFrameRate": 90000, - "Profile": "Baseline", - "Type": "EmbeddedImage", - "AspectRatio": "1:1", - "Index": 1, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "PixelFormat": "yuvj420p", - "Level": -99, - "IsAnamorphic": false - } - ], - "MediaAttachments": [], - "Formats": [], - "Bitrate": 297598, - "RequiredHttpHeaders": {}, - "TranscodingSubProtocol": "http", - "DefaultAudioStreamIndex": 0 - } - ], - "ChannelId": null, - "RunTimeTicks": 2464333790, - "ProductionYear": 2018, - "IndexNumber": 1, - "ParentIndexNumber": 1, - "ProviderIds": {}, - "IsFolder": false, - "Type": "Audio", - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "Dead Like Harry-Where the Bands Are (2018 Version) - Single-0001-0001Where the Bands Are (2018 Version)" - }, - "Artists": [ - "Dead Like Harry" - ], - "ArtistItems": [ - { - "Name": "Dead Like Harry", - "Id": "94875b0dd58cbf5245a135982133651a" - } - ], - "Album": "Where the Bands Are (2018 Version) - Single", - "AlbumArtist": "Dead Like Harry", - "AlbumArtists": [ - { - "Name": "Dead Like Harry", - "Id": "94875b0dd58cbf5245a135982133651a" - } - ], - "MediaStreams": [ - { - "Codec": "aac", - "CodecTag": "mp4a", - "Language": "eng", - "TimeBase": "1/44100", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "English - AAC - Stereo - Default", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 278038, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": true, - "IsForced": false, - "IsHearingImpaired": false, - "Profile": "LC", - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - }, - { - "Codec": "mjpeg", - "ColorSpace": "bt470bg", - "TimeBase": "1/90000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "IsInterlaced": false, - "IsAVC": false, - "BitDepth": 8, - "RefFrames": 1, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Height": 600, - "Width": 600, - "RealFrameRate": 90000, - "Profile": "Baseline", - "Type": "EmbeddedImage", - "AspectRatio": "1:1", - "Index": 1, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "PixelFormat": "yuvj420p", - "Level": -99, - "IsAnamorphic": false - } - ], - "ImageTags": { - "Primary": "dbd792d6c27313d01ed7c2dce85f785b" - }, - "BackdropImageTags": [], - "ImageBlurHashes": { - "Primary": { - "dbd792d6c27313d01ed7c2dce85f785b": "eXI|wC^*={t6-o_3o#o#oft7~WtRbwNHS5?bS$ozaeR-o}WXt7jYR+" - } - }, - "LocationType": "FileSystem", - "MediaType": "Audio", - "NormalizationGain": -10.7 -} diff --git a/tests/server/providers/jellyfin/fixtures/tracks/zombie_christmas.json b/tests/server/providers/jellyfin/fixtures/tracks/zombie_christmas.json deleted file mode 100644 index 24207dbc..00000000 --- a/tests/server/providers/jellyfin/fixtures/tracks/zombie_christmas.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "Name": "Zombie Christmas", - "ServerId": "58f180d8d2b34927bcfd73eee400ffad", - "Id": "fb12a77f49616a9fc56a6fab2b94174c", - "CanDownload": true, - "HasLyrics": false, - "Container": "mov,mp4,m4a,3gp,3g2,mj2", - "SortName": "0001 - 0008 - Zombie Christmas", - "PremiereDate": "2011-11-21T00:00:00.0000000Z", - "MediaSources": [ - { - "Protocol": "File", - "Id": "fb12a77f49616a9fc56a6fab2b94174c", - "Path": "/media/music/Emmy the Great & Tim Wheeler/This Is Christmas/8. Zombie Christmas.m4a", - "Type": "Default", - "Container": "m4a", - "Size": 8225981, - "Name": "8. Zombie Christmas", - "IsRemote": false, - "ETag": "0185e75e1fdad95cb227ce8d815d8cb5", - "RunTimeTicks": 2249317010, - "ReadAtNativeFramerate": false, - "IgnoreDts": false, - "IgnoreIndex": false, - "GenPtsInput": false, - "SupportsTranscoding": true, - "SupportsDirectStream": true, - "SupportsDirectPlay": true, - "IsInfiniteStream": false, - "RequiresOpening": false, - "RequiresClosing": false, - "RequiresLooping": false, - "SupportsProbing": true, - "MediaStreams": [ - { - "Codec": "aac", - "CodecTag": "mp4a", - "Language": "eng", - "TimeBase": "1/44100", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "English - AAC - Stereo - Default", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 267933, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": true, - "IsForced": false, - "IsHearingImpaired": false, - "Profile": "LC", - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - }, - { - "Codec": "mjpeg", - "ColorSpace": "bt470bg", - "TimeBase": "1/90000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "IsInterlaced": false, - "IsAVC": false, - "BitDepth": 8, - "RefFrames": 1, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Height": 449, - "Width": 500, - "RealFrameRate": 90000, - "Profile": "Baseline", - "Type": "EmbeddedImage", - "AspectRatio": "500:449", - "Index": 1, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "PixelFormat": "yuvj420p", - "Level": -99, - "IsAnamorphic": false - } - ], - "MediaAttachments": [], - "Formats": [], - "Bitrate": 292568, - "RequiredHttpHeaders": {}, - "TranscodingSubProtocol": "http", - "DefaultAudioStreamIndex": 0 - } - ], - "ChannelId": null, - "RunTimeTicks": 2249317010, - "ProductionYear": 2011, - "IndexNumber": 8, - "ParentIndexNumber": 1, - "ProviderIds": { - "MusicBrainzArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0", - "MusicBrainzAlbumArtist": "60bbceb2-0ddc-403b-970b-b4e9c3b2de5c/827b9ff1-56f8-4614-9261-a08de5fc1be0", - "MusicBrainzAlbum": "b13a174d-527d-44a1-b8f8-a4c78b03b7d9", - "MusicBrainzReleaseGroup": "f002d6b7-17af-4f9e-8d30-5486548ffe6f", - "MusicBrainzTrack": "17d1019d-d4f4-326c-b4bb-d8aec2607bd7" - }, - "IsFolder": false, - "Type": "Audio", - "UserData": { - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": false, - "Played": false, - "Key": "Emmy the Great & Tim Wheeler-This Is Christmas-0001-0008Zombie Christmas" - }, - "Artists": [ - "Emmy the Great", - "Tim Wheeler" - ], - "ArtistItems": [ - { - "Name": "Emmy the Great", - "Id": "a0c459294295710546c81c20a8d9abfc" - }, - { - "Name": "Tim Wheeler", - "Id": "1952db245ddef4e41dcd016475379190" - } - ], - "Album": "This Is Christmas", - "AlbumId": "32ed6a0091733dcff57eae67010f3d4b", - "AlbumPrimaryImageTag": "b685ba2b9247aca1ea66dda557bb8f54", - "AlbumArtist": "Emmy the Great & Tim Wheeler", - "AlbumArtists": [ - { - "Name": "Emmy the Great & Tim Wheeler", - "Id": "555b36f7d310d1b7405557a8775c6878" - } - ], - "MediaStreams": [ - { - "Codec": "aac", - "CodecTag": "mp4a", - "Language": "eng", - "TimeBase": "1/44100", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "DisplayTitle": "English - AAC - Stereo - Default", - "IsInterlaced": false, - "IsAVC": false, - "ChannelLayout": "stereo", - "BitRate": 267933, - "Channels": 2, - "SampleRate": 44100, - "IsDefault": true, - "IsForced": false, - "IsHearingImpaired": false, - "Profile": "LC", - "Type": "Audio", - "Index": 0, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "Level": 0 - }, - { - "Codec": "mjpeg", - "ColorSpace": "bt470bg", - "TimeBase": "1/90000", - "VideoRange": "Unknown", - "VideoRangeType": "Unknown", - "AudioSpatialFormat": "None", - "IsInterlaced": false, - "IsAVC": false, - "BitDepth": 8, - "RefFrames": 1, - "IsDefault": false, - "IsForced": false, - "IsHearingImpaired": false, - "Height": 449, - "Width": 500, - "RealFrameRate": 90000, - "Profile": "Baseline", - "Type": "EmbeddedImage", - "AspectRatio": "500:449", - "Index": 1, - "IsExternal": false, - "IsTextSubtitleStream": false, - "SupportsExternalStream": false, - "PixelFormat": "yuvj420p", - "Level": -99, - "IsAnamorphic": false - } - ], - "ImageTags": { - "Primary": "c8e39ff125c3ba39a5791570dffa4b83" - }, - "BackdropImageTags": [], - "ImageBlurHashes": { - "Primary": { - "c8e39ff125c3ba39a5791570dffa4b83": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC", - "b685ba2b9247aca1ea66dda557bb8f54": "VGEB:8ogOrxt9G_MkC-AxaR*w1xaI:oe?GS~%1ixs:kC" - } - }, - "LocationType": "FileSystem", - "MediaType": "Audio", - "NormalizationGain": -11.9 -} diff --git a/tests/server/providers/jellyfin/test_init.py b/tests/server/providers/jellyfin/test_init.py deleted file mode 100644 index 1222ef58..00000000 --- a/tests/server/providers/jellyfin/test_init.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Tests for the Jellyfin provider.""" - -from collections.abc import AsyncGenerator -from unittest import mock - -import pytest -from aiojellyfin.testing import FixtureBuilder - -from music_assistant.common.models.config_entries import ProviderConfig -from music_assistant.server.server import MusicAssistant -from tests.common import get_fixtures_dir, wait_for_sync_completion - - -@pytest.fixture -async def jellyfin_provider(mass: MusicAssistant) -> AsyncGenerator[ProviderConfig, None]: - """Configure an aiojellyfin test fixture, and add a provider to mass that uses it.""" - f = FixtureBuilder() - async for _, artist in get_fixtures_dir("artists", "jellyfin"): - f.add_json_bytes(artist) - - async for _, album in get_fixtures_dir("albums", "jellyfin"): - f.add_json_bytes(album) - - async for _, track in get_fixtures_dir("tracks", "jellyfin"): - f.add_json_bytes(track) - - authenticate_by_name = f.to_authenticate_by_name() - - with mock.patch( - "music_assistant.server.providers.jellyfin.authenticate_by_name", authenticate_by_name - ): - async with wait_for_sync_completion(mass): - config = await mass.config.save_provider_config( - "jellyfin", - { - "url": "http://localhost", - "username": "username", - "password": "password", - }, - ) - - yield config - - -@pytest.mark.usefixtures("jellyfin_provider") -async def test_initial_sync(mass: MusicAssistant) -> None: - """Test that initial sync worked.""" - artists = await mass.music.artists.library_items(search="Ash") - assert artists[0].name == "Ash" - - albums = await mass.music.albums.library_items(search="christmas") - assert albums[0].name == "This Is Christmas" - - tracks = await mass.music.tracks.library_items(search="where the bands are") - assert tracks[0].name == "Where the Bands Are (2018 Version)" diff --git a/tests/server/providers/jellyfin/test_parsers.py b/tests/server/providers/jellyfin/test_parsers.py deleted file mode 100644 index 12a49457..00000000 --- a/tests/server/providers/jellyfin/test_parsers.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Test we can parse Jellyfin models into Music Assistant models.""" - -import logging -import pathlib -from collections.abc import AsyncGenerator - -import aiofiles -import aiohttp -import pytest -from aiojellyfin import Artist, Connection, SessionConfiguration -from mashumaro.codecs.json import JSONDecoder -from syrupy.assertion import SnapshotAssertion - -from music_assistant.server.providers.jellyfin.parsers import parse_album, parse_artist, parse_track - -FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" -ARTIST_FIXTURES = list(FIXTURES_DIR.glob("artists/*.json")) -ALBUM_FIXTURES = list(FIXTURES_DIR.glob("albums/*.json")) -TRACK_FIXTURES = list(FIXTURES_DIR.glob("tracks/*.json")) - -ARTIST_DECODER = JSONDecoder(Artist) - -_LOGGER = logging.getLogger(__name__) - - -@pytest.fixture -async def connection() -> AsyncGenerator[Connection, None]: - """Spin up a dummy connection.""" - async with aiohttp.ClientSession() as session: - session_config = SessionConfiguration( - session=session, - url="http://localhost:1234", - app_name="X", - app_version="0.0.0", - device_id="X", - device_name="localhost", - ) - yield Connection(session_config, "USER_ID", "ACCESS_TOKEN") - - -@pytest.mark.parametrize("example", ARTIST_FIXTURES, ids=lambda val: str(val.stem)) -async def test_parse_artists( - example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion -) -> None: - """Test we can parse artists.""" - async with aiofiles.open(example) as fp: - raw_data = ARTIST_DECODER.decode(await fp.read()) - parsed = parse_artist(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() - # sort external Ids to ensure they are always in the same order for snapshot testing - parsed["external_ids"].sort() - assert snapshot == parsed - - -@pytest.mark.parametrize("example", ALBUM_FIXTURES, ids=lambda val: str(val.stem)) -async def test_parse_albums( - example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion -) -> None: - """Test we can parse albums.""" - async with aiofiles.open(example) as fp: - raw_data = ARTIST_DECODER.decode(await fp.read()) - parsed = parse_album(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() - # sort external Ids to ensure they are always in the same order for snapshot testing - parsed["external_ids"].sort() - assert snapshot == parsed - - -@pytest.mark.parametrize("example", TRACK_FIXTURES, ids=lambda val: str(val.stem)) -async def test_parse_tracks( - example: pathlib.Path, connection: Connection, snapshot: SnapshotAssertion -) -> None: - """Test we can parse tracks.""" - async with aiofiles.open(example) as fp: - raw_data = ARTIST_DECODER.decode(await fp.read()) - parsed = parse_track(_LOGGER, "xx-instance-id-xx", connection, raw_data).to_dict() - # sort external Ids to ensure they are always in the same order for snapshot testing - parsed["external_ids"] - assert snapshot == parsed diff --git a/tests/server/test_compare.py b/tests/server/test_compare.py deleted file mode 100644 index ff9eaad0..00000000 --- a/tests/server/test_compare.py +++ /dev/null @@ -1,369 +0,0 @@ -"""Tests for mediaitem compare helper functions.""" - -from music_assistant.common.models import media_items -from music_assistant.server.helpers import compare - - -def test_compare_version() -> None: - """Test the version compare helper.""" - assert compare.compare_version("Remaster", "remaster") is True - assert compare.compare_version("Remastered", "remaster") is True - assert compare.compare_version("Remaster", "") is False - assert compare.compare_version("Remaster", "Remix") is False - assert compare.compare_version("", "Deluxe") is False - assert compare.compare_version("", "Live") is False - assert compare.compare_version("Live", "live") is True - assert compare.compare_version("Live", "live version") is True - assert compare.compare_version("Live version", "live") is True - assert compare.compare_version("Deluxe Edition", "Deluxe") is True - assert compare.compare_version("Deluxe Karaoke Edition", "Deluxe") is False - assert compare.compare_version("Deluxe Karaoke Edition", "Karaoke") is False - assert compare.compare_version("Deluxe Edition", "Edition Deluxe") is True - assert compare.compare_version("", "Karaoke Version") is False - assert compare.compare_version("Karaoke", "Karaoke Version") is True - assert compare.compare_version("Remaster", "Remaster Edition Deluxe") is False - assert compare.compare_version("Remastered Version", "Deluxe Version") is False - - -def test_compare_artist() -> None: - """Test artist comparison.""" - artist_a = media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - artist_b = media_items.Artist( - item_id="1", - provider="test2", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="2", provider_domain="test", provider_instance="test2" - ) - }, - ) - # test match on name match - assert compare.compare_artist(artist_a, artist_b) is True - # test match on name mismatch - artist_b.name = "Artist B" - assert compare.compare_artist(artist_a, artist_b) is False - # test on exact item_id match - artist_b.item_id = artist_a.item_id - artist_b.provider = artist_a.provider - assert compare.compare_artist(artist_a, artist_b) is True - # test on external id match - artist_b.name = "Artist B" - artist_b.item_id = "2" - artist_b.provider = "test2" - artist_a.external_ids = {(media_items.ExternalID.MB_ARTIST, "123")} - artist_b.external_ids = artist_a.external_ids - assert compare.compare_artist(artist_a, artist_b) is True - # test on external id mismatch - artist_b.name = artist_a.name - artist_b.external_ids = {(media_items.ExternalID.MB_ARTIST, "1234")} - assert compare.compare_artist(artist_a, artist_b) is False - # test on external id mismatch while name matches - artist_a = media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - external_ids={(media_items.ExternalID.MB_ARTIST, "123")}, - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - artist_b = media_items.Artist( - item_id="1", - provider="test2", - name="Artist A", - external_ids={(media_items.ExternalID.MB_ARTIST, "abc")}, - provider_mappings={ - media_items.ProviderMapping( - item_id="2", provider_domain="test", provider_instance="test2" - ) - }, - ) - assert compare.compare_artist(artist_a, artist_b) is False - - -def test_compare_album() -> None: - """Test album comparison.""" - album_a = media_items.Album( - item_id="1", - provider="test1", - name="Album A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - artists=media_items.UniqueList( - [ - media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - ] - ), - ) - album_b = media_items.Album( - item_id="1", - provider="test2", - name="Album A", - provider_mappings={ - media_items.ProviderMapping( - item_id="2", provider_domain="test", provider_instance="test2" - ) - }, - artists=media_items.UniqueList( - [ - media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - ] - ), - ) - # test match on name match - assert compare.compare_album(album_a, album_b) is True - # test match on name mismatch - album_b.name = "Album B" - assert compare.compare_album(album_a, album_b) is False - # test on version mismatch - album_b.name = album_a.name - album_b.version = "Deluxe" - assert compare.compare_album(album_a, album_b) is False - album_b.version = "Remix" - assert compare.compare_album(album_a, album_b) is False - # test on version match - album_b.name = album_a.name - album_a.version = "Deluxe" - album_b.version = "Deluxe Edition" - assert compare.compare_album(album_a, album_b) is True - # test on exact item_id match - album_b.item_id = album_a.item_id - album_b.provider = album_a.provider - assert compare.compare_album(album_a, album_b) is True - # test on external id match - album_b.name = "Album B" - album_b.item_id = "2" - album_b.provider = "test2" - album_a.external_ids = {(media_items.ExternalID.MB_ALBUM, "123")} - album_b.external_ids = album_a.external_ids - assert compare.compare_album(album_a, album_b) is True - # test on external id mismatch - album_b.name = album_a.name - album_b.external_ids = {(media_items.ExternalID.MB_ALBUM, "1234")} - assert compare.compare_album(album_a, album_b) is False - album_a.external_ids = set() - album_b.external_ids = set() - # fail on year mismatch - album_b.external_ids = set() - album_a.year = 2021 - album_b.year = 2020 - assert compare.compare_album(album_a, album_b) is False - # pass on year match - album_b.year = 2021 - assert compare.compare_album(album_a, album_b) is True - # fail on artist mismatch - album_a.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] - ) - album_b.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")] - ) - assert compare.compare_album(album_a, album_b) is False - # pass on partial artist match (if first artist matches) - album_a.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] - ) - album_b.artists = media_items.UniqueList( - [ - media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), - media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), - ] - ) - assert compare.compare_album(album_a, album_b) is True - # fail on partial artist match in strict mode - album_b.artists = media_items.UniqueList( - [ - media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), - media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), - ] - ) - assert compare.compare_album(album_a, album_b) is False - # partial artist match is allowed in non-strict mode - assert compare.compare_album(album_a, album_b, False) is True - - -def test_compare_track() -> None: # noqa: PLR0915 - """Test track comparison.""" - track_a = media_items.Track( - item_id="1", - provider="test1", - name="Track A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - artists=media_items.UniqueList( - [ - media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - ] - ), - ) - track_b = media_items.Track( - item_id="1", - provider="test2", - name="Track A", - provider_mappings={ - media_items.ProviderMapping( - item_id="2", provider_domain="test", provider_instance="test2" - ) - }, - artists=media_items.UniqueList( - [ - media_items.Artist( - item_id="1", - provider="test1", - name="Artist A", - provider_mappings={ - media_items.ProviderMapping( - item_id="1", provider_domain="test", provider_instance="test1" - ) - }, - ) - ] - ), - ) - # test match on name match - assert compare.compare_track(track_a, track_b) is True - # test match on name mismatch - track_b.name = "Track B" - assert compare.compare_track(track_a, track_b) is False - # test on version mismatch - track_b.name = track_a.name - track_b.version = "Deluxe" - assert compare.compare_track(track_a, track_b) is False - track_b.version = "Remix" - assert compare.compare_track(track_a, track_b) is False - # test on version mismatch - track_b.name = track_a.name - track_a.version = "" - track_b.version = "Remaster" - assert compare.compare_track(track_a, track_b) is False - track_b.version = "Remix" - assert compare.compare_track(track_a, track_b) is False - # test on version match - track_b.name = track_a.name - track_a.version = "Deluxe" - track_b.version = "Deluxe Edition" - assert compare.compare_track(track_a, track_b) is True - # test on exact item_id match - track_b.item_id = track_a.item_id - track_b.provider = track_a.provider - assert compare.compare_track(track_a, track_b) is True - # test on external id match - track_b.name = "Track B" - track_b.item_id = "2" - track_b.provider = "test2" - track_a.external_ids = {(media_items.ExternalID.MB_RECORDING, "123")} - track_b.external_ids = track_a.external_ids - assert compare.compare_track(track_a, track_b) is True - # test on external id mismatch - track_b.name = track_a.name - track_b.external_ids = {(media_items.ExternalID.MB_RECORDING, "1234")} - assert compare.compare_track(track_a, track_b) is False - track_a.external_ids = set() - track_b.external_ids = set() - # fail on artist mismatch - track_a.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] - ) - track_b.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")] - ) - assert compare.compare_track(track_a, track_b) is False - # pass on partial artist match (if first artist matches) - track_a.artists = media_items.UniqueList( - [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")] - ) - track_b.artists = media_items.UniqueList( - [ - media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), - media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), - ] - ) - assert compare.compare_track(track_a, track_b) is True - # fail on partial artist match in strict mode - track_b.artists = media_items.UniqueList( - [ - media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"), - media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"), - ] - ) - assert compare.compare_track(track_a, track_b) is False - # partial artist match is allowed in non-strict mode - assert compare.compare_track(track_a, track_b, False) is True - track_b.artists = track_a.artists - # fail on album mismatch - track_a.album = media_items.ItemMapping(item_id="1", provider="test1", name="Album A") - track_b.album = media_items.ItemMapping(item_id="2", provider="test1", name="Album B") - assert compare.compare_track(track_a, track_b) is False - # pass on exact album(track) match (regardless duration) - track_b.album = track_a.album - track_a.disc_number = 1 - track_a.track_number = 1 - track_b.disc_number = track_a.disc_number - track_b.track_number = track_a.track_number - track_a.duration = 300 - track_b.duration = 310 - assert compare.compare_track(track_a, track_b) is True - # pass on album(track) mismatch - track_b.album = track_a.album - track_a.disc_number = 1 - track_a.track_number = 1 - track_b.disc_number = track_a.disc_number - track_b.track_number = 2 - track_b.duration = track_a.duration - assert compare.compare_track(track_a, track_b) is False - # test special case - ISRC match but MusicBrainz ID mismatch - # this can happen for some classical music albums - track_a.external_ids = { - (media_items.ExternalID.ISRC, "123"), - (media_items.ExternalID.MB_RECORDING, "abc"), - } - track_b.external_ids = { - (media_items.ExternalID.ISRC, "123"), - (media_items.ExternalID.MB_RECORDING, "abcd"), - } - assert compare.compare_track(track_a, track_b) is False diff --git a/tests/server/test_server.py b/tests/server/test_server.py deleted file mode 100644 index ee1b58b8..00000000 --- a/tests/server/test_server.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Tests for the core Music Assistant server object.""" - -import asyncio - -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.event import MassEvent -from music_assistant.server.server import MusicAssistant - - -async def test_start_and_stop_server(mass: MusicAssistant) -> None: - """Test that music assistant starts and stops cleanly.""" - domains = frozenset(p.domain for p in mass.get_provider_manifests()) - core_providers = frozenset( - ("builtin", "cache", "metadata", "music", "player_queues", "players", "streams") - ) - assert domains.issuperset(core_providers) - - -async def test_events(mass: MusicAssistant) -> None: - """Test that events sent by signal_event can be seen by subscribe.""" - filters: list[tuple[EventType | tuple[EventType, ...] | None, str | tuple[str, ...] | None]] = [ - (None, None), - (EventType.UNKNOWN, None), - ((EventType.UNKNOWN, EventType.AUTH_SESSION), None), - (None, "myid1"), - (None, ("myid1", "myid2")), - (EventType.UNKNOWN, "myid1"), - ] - - for event_filter, id_filter in filters: - flag = False - - def _ev(event: MassEvent) -> None: - assert event.event == EventType.UNKNOWN - assert event.data == "mytestdata" - assert event.object_id == "myid1" - nonlocal flag - flag = True - - remove_cb = mass.subscribe(_ev, event_filter, id_filter) - - mass.signal_event(EventType.UNKNOWN, "myid1", "mytestdata") - await asyncio.sleep(0) - assert flag is True - - flag = False - remove_cb() - mass.signal_event(EventType.UNKNOWN) - await asyncio.sleep(0) - assert flag is False diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index a01934e5..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Tests for utility/helper functions.""" - -import pytest - -from music_assistant.common.helpers import uri, util -from music_assistant.common.models import media_items -from music_assistant.common.models.errors import MusicAssistantError -from music_assistant.constants import SILENCE_FILE - - -def test_version_extract() -> None: - """Test the extraction of version from title.""" - test_str = "Bam Bam (feat. Ed Sheeran)" - title, version = util.parse_title_and_version(test_str) - assert title == "Bam Bam" - assert version == "" - test_str = "Bam Bam (feat. Ed Sheeran) - Karaoke Version" - title, version = util.parse_title_and_version(test_str) - assert title == "Bam Bam" - assert version == "Karaoke Version" - test_str = "Bam Bam (feat. Ed Sheeran) [Karaoke Version]" - title, version = util.parse_title_and_version(test_str) - assert title == "Bam Bam" - assert version == "Karaoke Version" - test_str = "SuperSong (2011 Remaster)" - title, version = util.parse_title_and_version(test_str) - assert title == "SuperSong" - assert version == "2011 Remaster" - test_str = "SuperSong (Live at Wembley)" - title, version = util.parse_title_and_version(test_str) - assert title == "SuperSong" - assert version == "Live at Wembley" - test_str = "SuperSong (Instrumental)" - title, version = util.parse_title_and_version(test_str) - assert title == "SuperSong" - assert version == "Instrumental" - test_str = "SuperSong (Explicit)" - title, version = util.parse_title_and_version(test_str) - assert title == "SuperSong" - assert version == "" - - -async def test_uri_parsing() -> None: - """Test parsing of URI.""" - # test regular uri - test_uri = "spotify://track/123456789" - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.TRACK - assert provider == "spotify" - assert item_id == "123456789" - # test spotify uri - test_uri = "spotify:track:123456789" - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.TRACK - assert provider == "spotify" - assert item_id == "123456789" - # test public play/open url - test_uri = "https://open.spotify.com/playlist/5lH9NjOeJvctAO92ZrKQNB?si=04a63c8234ac413e" - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.PLAYLIST - assert provider == "spotify" - assert item_id == "5lH9NjOeJvctAO92ZrKQNB" - # test filename with slashes as item_id - test_uri = "filesystem://track/Artist/Album/Track.flac" - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.TRACK - assert provider == "filesystem" - assert item_id == "Artist/Album/Track.flac" - # test regular url to builtin provider - test_uri = "http://radiostream.io/stream.mp3" - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.UNKNOWN - assert provider == "builtin" - assert item_id == "http://radiostream.io/stream.mp3" - # test local file to builtin provider - test_uri = SILENCE_FILE - media_type, provider, item_id = await uri.parse_uri(test_uri) - assert media_type == media_items.MediaType.UNKNOWN - assert provider == "builtin" - assert item_id == SILENCE_FILE - # test invalid uri - with pytest.raises(MusicAssistantError): - await uri.parse_uri("invalid://blah") diff --git a/tests/test_radio_stream_title.py b/tests/test_radio_stream_title.py deleted file mode 100644 index 8dbb5f9d..00000000 --- a/tests/test_radio_stream_title.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Tests for cleaning radio streamtitle.""" - -from music_assistant.common.helpers.util import clean_stream_title - - -def test_cleaning_streamtitle() -> None: - """Tests for cleaning radio streamtitle.""" - tstm = "Thirty Seconds To Mars - Closer to the Edge" - advert = "Advert" - - line = "Advertisement_Start_Length=00:00:29.960" - stream_title = clean_stream_title(line) - assert stream_title == advert - - line = "Advertisement_Stop" - stream_title = clean_stream_title(line) - assert stream_title == advert - - line = "START_AD_BREAK_6000" - stream_title = clean_stream_title(line) - assert stream_title == advert - - line = "STOP ADBREAK 1" - stream_title = clean_stream_title(line) - assert stream_title == advert - - line = "AD 2" - stream_title = clean_stream_title(line) - assert stream_title == advert - - line = 'title="Thirty Seconds To Mars - Closer to the Edge",artist="Thirty Seconds To Mars - Closer to the Edge",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' # noqa: E501 - stream_title = clean_stream_title(line) - assert stream_title == tstm - - line = 'title="https://listenapi.planetradio.co.uk/api9.2/eventdata/247801912",url="https://listenapi.planetradio.co.uk/api9.2/eventdata/247801912"' - stream_title = clean_stream_title(line) - assert stream_title == "" - - line = 'title="Thirty Seconds To Mars - Closer to the Edge https://nowplaying.scahw.com.au/",artist="Thirty Seconds To Mars - Closer to the Edge",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' # noqa: E501 - stream_title = clean_stream_title(line) - assert stream_title == tstm - - line = 'title="Closer to the Edge",artist="Thirty Seconds To Mars",url="https://nowplaying.scahw.com.au/c/fd8ee07bed6a5e4e9824a11aa02dd34a.jpg?t=1714568458&l=250"' - stream_title = clean_stream_title(line) - assert stream_title == tstm - - line = 'title="Thirty Seconds To Mars - Closer to the Edge"' - stream_title = clean_stream_title(line) - assert stream_title == tstm - - line = "Thirty Seconds To Mars - Closer to the Edge https://nowplaying.scahw.com.au/" - stream_title = clean_stream_title(line) - assert stream_title == tstm - - line = "Lonely Street By: Andy Williams - WALMRadio.com" - stream_title = clean_stream_title(line) - assert stream_title == "Andy Williams - Lonely Street" - - line = "Bye Bye Blackbird By: Sammy Davis Jr. - WALMRadio.com" - stream_title = clean_stream_title(line) - assert stream_title == "Sammy Davis Jr. - Bye Bye Blackbird" - - line = ( - "Asha Bhosle, Mohd Rafi (mp3yaar.com) - Gunguna Rahe Hain Bhanwre - Araadhna (mp3yaar.com)" - ) - stream_title = clean_stream_title(line) - assert stream_title == "Asha Bhosle, Mohd Rafi - Gunguna Rahe Hain Bhanwre - Araadhna" - - line = "Mohammed Rafi(Jatt.fm) - Rang Aur Noor Ki Baraat (Ghazal)(Jatt.fm)" - stream_title = clean_stream_title(line) - assert stream_title == "Mohammed Rafi - Rang Aur Noor Ki Baraat (Ghazal)" diff --git a/tests/test_tags.py b/tests/test_tags.py deleted file mode 100644 index be9a94f9..00000000 --- a/tests/test_tags.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Tests for parsing ID3 tags functions.""" - -import pathlib - -from music_assistant.server.helpers import tags - -RESOURCES_DIR = pathlib.Path(__file__).parent.resolve().joinpath("fixtures") - -FILE_1 = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3")) - - -async def test_parse_metadata_from_id3tags() -> None: - """Test parsing of parsing metadata from ID3 tags.""" - filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3")) - _tags = await tags.parse_tags(filename) - assert _tags.album == "MyAlbum" - assert _tags.title == "MyTitle" - assert _tags.duration == 1.032 - assert _tags.album_artists == ("MyArtist",) - assert _tags.artists == ("MyArtist", "MyArtist2") - assert _tags.genres == ("Genre1", "Genre2") - assert _tags.musicbrainz_albumartistids == ("abcdefg",) - assert _tags.musicbrainz_artistids == ("abcdefg",) - assert _tags.musicbrainz_releasegroupid == "abcdefg" - assert _tags.musicbrainz_recordingid == "abcdefg" - # test parsing disc/track number - _tags.tags["disc"] = "" - assert _tags.disc is None - _tags.tags["disc"] = "1" - assert _tags.disc == 1 - _tags.tags["disc"] = "1/1" - assert _tags.disc == 1 - # test parsing album year - _tags.tags["date"] = "blah" - assert _tags.year is None - _tags.tags.pop("date", None) - assert _tags.year is None - _tags.tags["date"] = "2022" - assert _tags.year == 2022 - _tags.tags["date"] = "2022-05-05" - assert _tags.year == 2022 - _tags.tags["date"] = "" - assert _tags.year is None - - -async def test_parse_metadata_from_filename() -> None: - """Test parsing of parsing metadata from filename.""" - filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle without Tags.mp3")) - _tags = await tags.parse_tags(filename) - assert _tags.album is None - assert _tags.title == "MyTitle without Tags" - assert _tags.duration == 1.032 - assert _tags.album_artists == () - assert _tags.artists == ("MyArtist",) - assert _tags.genres == () - assert _tags.musicbrainz_albumartistids == () - assert _tags.musicbrainz_artistids == () - assert _tags.musicbrainz_releasegroupid is None - assert _tags.musicbrainz_recordingid is None - - -async def test_parse_metadata_from_invalid_filename() -> None: - """Test parsing of parsing metadata from (invalid) filename.""" - filename = str(RESOURCES_DIR.joinpath("test.mp3")) - _tags = await tags.parse_tags(filename) - assert _tags.album is None - assert _tags.title == "test" - assert _tags.duration == 1.032 - assert _tags.album_artists == () - assert _tags.artists == (tags.UNKNOWN_ARTIST,) - assert _tags.genres == () - assert _tags.musicbrainz_albumartistids == () - assert _tags.musicbrainz_artistids == () - assert _tags.musicbrainz_releasegroupid is None - assert _tags.musicbrainz_recordingid is None